【unity小技巧】unity最完美的CharacterController 3d角色控制器,实现移动、跳跃、下蹲、奔跑、上下坡、物理碰撞效果,复制粘贴即用

简介: 【unity小技巧】unity最完美的CharacterController 3d角色控制器,实现移动、跳跃、下蹲、奔跑、上下坡、物理碰撞效果,复制粘贴即用

前言

其实一开始我是不打算写的,我感觉这种简单的功能,网上应该随便一搜一大堆,但是实际去搜会发现网上很多都是复制粘贴,要么没有实操过,要么就是功能不全,或者毫无解释的把代码丢出来,所以特地花时间去研究了研究,下面是记录我的一些思路过程,希望对你有帮助。

其实之前实战有做过FPS移动控制,只是没有说的很全面,感兴趣可以查看之前的文章:

【用unity实现100个游戏之18】从零开始制作一个类CSGO/CS2、CF第一人称FPS射击游戏——基础篇1

为什么使用CharacterController

Unity中常用的三种角色移动方式如下:

  1. 使用刚体(Rigidbody)组件:这种方式将角色对象添加刚体组件,并通过力(Force)或者速度(Velocity)来控制移动。你可以使用输入控制器(例如键盘、手柄)获取移动输入,然后将对应的力或速度施加给角色刚体,从而实现移动。
  2. 使用Transform组件的Translate方法:这种方式直接使用Transform组件的Translate方法来移动角色。你可以通过获取输入控制器的输入,计算出移动方向和距离,然后调用Translate方法将角色移动到指定位置。
  3. 使用Character Controller组件:这种方式需要将角色对象添加Character Controller组件,并使用Character Controller提供的Move方法来移动角色。你可以通过输入控制器获取移动输入,然后将输入转换为移动向量,并传递给Character Controller的Move方法来实现移动。

刚体自带重力和物理效果,但是对于爬坡,走楼梯要单独处理,比较麻烦。(ps:当然,后面有机会我在研究Rigidbody如何控制人物,可以关注期待一下

Transform呢,不带重力又不带碰撞,移动不受物理引擎控制,可能导致穿透墙壁或其他对象。不支持碰撞和重力等物理效果。所以我是直接pass的

CharacterController主要是不适用于需要处理复杂物理交互的情况,例如推动物体等。但是对于爬坡,楼梯自带了处理方式,完美解决了刚体的痛点,而如果你想推动物体也可以直接给物体一个推力即可解决(文章最后面我分享解决方案

SimpleMove和Move如何选择?

而对于CharacterController有用两种移动实现方式,SimpleMove和Move。

1. SimpleMove

  • 不受Y轴速度影响,自带重力效果,无法实现跳跃功能
  • 返回值为Bool,当角色接触地面返回True,反之为False。
  • SimpleMove方法是CharacterController组件提供的一个用于处理角色平面移动的简化方法,它自动处理了角色与地面的碰撞* 检测和摩擦,但它不支持跳跃等垂直方向的动作

2. Move

  • 无重力效果,自行实现重力,可做跳跃功能
  • 返回值(CollisionFlags对象),返回角色与物体碰撞的信息

可以看到SimpleMove看着虽好,但是最大的痛点是无法实现跳跃,所以我们只能忍痛Pass掉

配置CharacterController参数

新增一个胶囊体,代表角色,在角色身上新增CharacterController组件,参数配置如下

控制相机

将相机拖入为玩家的子物体,放置在角色的头部位置,修改

新增MouseLook脚本,挂载在相机上,控制相机视角

public class MouseLook : MonoBehaviour
{
    // 鼠标灵敏度
    public float mouseSensitivity = 1000f;
    // 玩家的身体Transform组件,用于旋转
    public Transform playerBody;
    // x轴的旋转角度
    float xRotation = 0f;
    void Start()
    {
        // 锁定光标到屏幕中心,并隐藏光标
        Cursor.lockState = CursorLockMode.Locked;
    }
    // Update在每一帧调用
    void Update()
    {
        // 执行自由视角查看功能
        FreeLook();
    }
    // 自由视角查看功能的实现
    void FreeLook()
    {
        // 获取鼠标X轴和Y轴的移动量,乘以灵敏度和时间,得到平滑的移动速率
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
        //限制旋转角度在-90到90度之间,防止过度翻转
        xRotation = Mathf.Clamp(xRotation, -90f, 90f);
        // 累计x轴上的旋转量
        xRotation -= mouseY;
        // 应用摄像头的x轴旋转
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
        // 应用玩家身体的y轴旋转
        playerBody.Rotate(Vector3.up * mouseX);
    }
}

效果

移动

经过上面的分享,我们使用Move实现一下人物的移动

新增MovementScript脚本,挂载在角色身上

public class MovementScript : MonoBehaviour
{
  [Tooltip("角色控制器")] public CharacterController characterController;
  private float horizontal;
    private float vertical;
    
    [Header("移动")]
    [Tooltip("角色行走的速度")] public float walkSpeed = 6f;
    [Tooltip("当前速度")] private float speed;
    [Tooltip("角色移动的方向")] private Vector3 moveDirection;
    
    void Start()
    {
        speed = walkSpeed;
    }
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
        moveDirection = transform.right * horizontal + transform.forward * vertical; // 计算移动方向
        //将该向量从局部坐标系转换为世界坐标系,得到最终的移动方向,效果和上面的一样
        // moveDirection = transform.TransformDirection(new Vector3(h, 0, v));
        moveDirection = moveDirection.normalized; // 归一化移动方向,避免斜向移动速度过快
        characterController.Move(moveDirection * Time.deltaTime * speed);
    }
}

效果

跳跃

方式一

补充:使用CharaterController.IsGrounded实现地面检测,更加简单,但是这个方式虽然简单,但是下坡时检测可能出现问题,导致跳不起来(也可能是我使用方式不对),如果你不在乎这个影响,可以酌情选择,因为它的使用真的很简单

void Update(){
  //地面检测
    isGround = characterController.isGrounded;
    SetJump();
}
//控制跳跃
void SetJump()
{
    bool jump = Input.GetButtonDown("Jump");
    if (isGround)
    {
        // 在着地时阻止垂直速度无限下降
        if (_verticalVelocity < 0.0f)
        {
            _verticalVelocity = -2f;
        }
        if (jump)
        {
            _verticalVelocity = jumpHeight;
        }
    }
    else
    {
        //随时间施加重力
        _verticalVelocity += Gravity * Time.deltaTime;
    }
    characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}

方式二

地面检测我们就用圆形球体把,因为人物是胶囊体,这种方法最合适

[Header("地面检测")]
[Tooltip("地面检测位置")] public Transform groundCheck;
[Tooltip("地面检测半径")] public float sphereRadius = 0.5f;
[Tooltip("是否在地面")] private bool isGround;
[Header("跳跃")]
[Tooltip("角色跳跃的高度")] public float jumpHeight = 1.5f;
[Tooltip("判断是否在跳跃")] private bool isJumping;
private float _verticalVelocity;
void Update()
{
  //地面检测
  isGround = IsGrounded();
  SetJump();
}
//控制跳跃
void SetJump()
{
     bool jump = Input.GetButtonDown("Jump");
     if (isGround)
      {
          isJumping = false;
          // 在着地时阻止垂直速度无限下降
          if (_verticalVelocity < 0.0f)
          {
              _verticalVelocity = -2f;
          }
          // 跳跃
          if (jump)
          {
              // H * -2 * G 的平方根 = 达到期望高度所需的速度
              _verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * Gravity);
          }
      }
      else
      {
          isJumping = true;
      }
      // 随时间施加重力
      _verticalVelocity += Gravity * Time.deltaTime;
}
//是否在地面
bool IsGrounded()
{
    Collider[] colliders = Physics.OverlapSphere(groundCheck.position, sphereRadius);
    foreach (Collider collider in colliders)
    {
        if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform)) // 忽略角色自身和所有子集碰撞体
        {
            return true;
        }
    }
    return false;
}
//判断child是否是parent的子集
bool IsChildOf(Transform child, Transform parent)
{
    while (child != null)
    {
        if (child == parent)
        {
            return true;
        }
        child = child.parent;
    }
    return false;
}
//在场景视图显示检测,方便调试
private void OnDrawGizmos()
{
    Gizmos.color = Color.red;
    
    //地面检测可视化
    Gizmos.DrawWireSphere(groundCheck.position, sphereRadius);
}

配置地面检测点位置

效果

下蹲

下蹲的逻辑就是让CharacterController 的高度减半,还有中心点的位置也跟着减半,当然还有摄像机的高度,还需要注意的是人物如果头顶有东西的时候我们是不允许他起立的,不然会穿模,所以还需要一个头顶检测,头顶我们使用盒子检测最好,可以覆盖整个头部

[Header("相机")]
[Tooltip("摄像机相机")] public Transform mainCamera;
[Tooltip("摄像机高度变化的平滑值")] public float interpolationSpeed = 10f;
[Tooltip("当前摄像机的位置")] private Vector3 cameraLocalPosition;
[Tooltip("当前摄像机的高度")] private float height;
[Header("头顶检测")]
[Tooltip("头顶检测位置")] public Transform headCheck;
[Tooltip("盒子半长、半宽、半高")] public Vector3 halfExtents = new Vector3(0.4f, 0.5f, 0.4f);
[Tooltip("判断玩家是否可以站立")] private bool isCanStand;
[Header("下蹲")]
[Tooltip("下蹲时候的玩家高度")] private float crouchHeight;
[Tooltip("判断玩家是否在下蹲")] private bool isCrouching;
[Tooltip("正常站立时玩家高度")] private float standHeight;
void Start()
{
    standHeight = characterController.height;
    crouchHeight = standHeight / 2;
    cameraLocalPosition = mainCamera.localPosition;
    speed = walkSpeed;
}
void Update()
{
    //头顶检测
    isCanStand = CanStand();
    SetCrouch();
}
    
//控制下蹲
void SetCrouch()
{
    if (Input.GetKey(KeyCode.LeftControl))
    {
        Crouch(true);
    }
    else
    {
        Crouch(false);
    }
}
//newCrouching控制下蹲起立
public void Crouch(bool newCrouching)
{
    if (!newCrouching && !isCanStand) return; //准备起立时且头顶有东西,不能进行站立
    isCrouching = newCrouching;
    float targetHeight = isCrouching ? crouchHeight : standHeight;
    characterController.height = targetHeight; //根据下蹲状态设置下蹲时候的高度和站立的高度
    characterController.center = new Vector3(0, targetHeight / 2, 0); //将角色控制器的中心位置Y,从头顶往下减少1半的高度
  // 设置下蹲站立时候的摄像机高度
    float heightTarget = isCrouching ? cameraLocalPosition.y / 2 + characterController.center.y : cameraLocalPosition.y;
    height = Mathf.Lerp(height, heightTarget, interpolationSpeed * Time.deltaTime);//平滑过渡
    mainCamera.localPosition = new Vector3(cameraLocalPosition.x, height, cameraLocalPosition.z);
}
//是否可以起立,及头顶是否有物品
bool CanStand()
{
    Collider[] colliders = Physics.OverlapBox(headCheck.position, halfExtents);
    foreach (Collider collider in colliders)
    {
        //忽略角色自身和所有子集碰撞体
        if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform))
        {
            return false;
        }
    }
    return true;
}
//在场景视图显示检测,方便调试
private void OnDrawGizmos()
{
    Gizmos.color = Color.red;
    
    //头顶检测可视化
    Gizmos.DrawWireCube(headCheck.position, halfExtents * 2f);
}

配置头顶检测

效果

image.png

处理下坡抖动问题

你的人物在下斜坡时可能会出现一个问题,人物下坡出现抖动,抖动其实本身可以理解,但是这个下坡抖动会影响地面检测准度,导致我们移动下坡时可能跳不起来,当然这不是我们想要的,具体的处理思路就是当我们判断在斜面时,给人物一个向下的压力,让人物没那么容易离地,而判断在斜面的方法就从人物向下打一条射线,判断射线和地面的法线如果非90度就是在斜面了

[Header("斜坡检测")]
[Tooltip("斜坡射线长度")] public float slopeForceRayLength = 0.2f;
[Tooltip("是否在斜坡")] private bool isSlope;
[Header("斜坡")]
[Tooltip("走斜坡时向下施加的力度")] public float slopeForce = 6.0f;
void Update()
{
  //斜坡检测
  isSlope = OnSlope();
  SetJump();
}
//控制跳跃
void SetJump()
{
    //。。。
  
  //为了不影响跳跃,一定要在isJumping = false之前加力
    SetSlope();
    isJumping = false;
}
    
//控制斜坡
public void SetSlope()
{
    //如果处于斜坡
    if (isSlope && !isJumping)
    {
        //向下增加力
        moveDirection.y = characterController.height / 2 * slopeForceRayLength;
        characterController.Move(Vector3.down * characterController.height / 2 * slopeForce * Time.deltaTime);
    }
}
//是否在斜面
public bool OnSlope()
{
    RaycastHit hit;
    // 向下打出射线(检测是否在斜坡上)
    if (Physics.Raycast(transform.position + characterController.height / 2 * Vector3.down, Vector3.down, out hit, characterController.height / 2 * slopeForceRayLength))
    {
        // 如果接触到的点的法线,不在(0,1,0)的方向上,那么人物就在斜坡上
        if (hit.normal != Vector3.up)
            return true;
    }
    return false;
}
//在场景视图显示检测,方便调试
private void OnDrawGizmos()
{
    Gizmos.color = Color.red;
    //斜坡检测可视化
    Debug.DrawRay(transform.position + characterController.height / 2 * Vector3.down, Vector3.down * characterController.height / 2 * slopeForceRayLength, Color.blue);
}

斜坡检测线

效果

实现奔跑和不同移速控制

[Header("移动")]
[Tooltip("角色行走的速度")] public float walkSpeed = 6f;
[Tooltip("角色奔跑的速度")] public float runSpeed = 9f;
[Tooltip("角色下蹲的速度")] public float crouchSpeed = 3f;
[Tooltip("角色移动的方向")] private Vector3 moveDirection;
[Tooltip("当前速度")] private float speed;
[Tooltip("是否奔跑")] private bool isRun;
    
//速度设置
void SetSpeed()
{
    if (isRun)
    {
        speed = runSpeed;
    }
    else if (isCrouching)
    {
        speed = crouchSpeed;
    }
    else
    {
        speed = walkSpeed;
    }
}
//控制奔跑
void SetRun()
{
    if (Input.GetKey(KeyCode.LeftShift) && !isCrouching)
    {
        isRun = true;
    }
    else
    {
        isRun = false;
    }
}

效果

image.png

完整代码

注意:代码的参数都是经过我测试过的,复制即可使用,并不推荐大家修改,除非你知道自己在干什么

[RequireComponent(typeof(CharacterController))]
public class MovementScript : MonoBehaviour
{
    [Tooltip("角色控制器")] public CharacterController characterController;
    [Tooltip("重力加速度")] private float Gravity = 9.8f;
    private float horizontal;
    private float vertical;
    [Header("相机")]
    [Tooltip("摄像机相机")] public Transform mainCamera;
    [Tooltip("摄像机高度变化的平滑值")] public float interpolationSpeed = 10f;
    [Tooltip("当前摄像机的位置")] private Vector3 cameraLocalPosition;
    [Tooltip("当前摄像机的高度")] private float height;
    [Header("移动")]
    [Tooltip("角色行走的速度")] public float walkSpeed = 6f;
    [Tooltip("角色奔跑的速度")] public float runSpeed = 9f;
    [Tooltip("角色下蹲的速度")] public float crouchSpeed = 3f;
    [Tooltip("角色移动的方向")] private Vector3 moveDirection;
    [Tooltip("当前速度")] private float speed;
    [Tooltip("是否奔跑")] private bool isRun;
    [Header("地面检测")]
    [Tooltip("地面检测位置")] public Transform groundCheck;
    [Tooltip("地面检测半径")] public float sphereRadius = 0.4f;
    [Tooltip("是否在地面")] private bool isGround;
    [Header("头顶检测")]
    [Tooltip("头顶检测位置")] public Transform headCheck;
    [Tooltip("盒子半长、半宽、半高")] public Vector3 halfExtents = new Vector3(0.4f, 0.5f, 0.4f);
    [Tooltip("判断玩家是否可以站立")] private bool isCanStand;
    [Header("斜坡检测")]
    [Tooltip("斜坡射线长度")] public float slopeForceRayLength = 0.2f;
    [Tooltip("是否在斜坡")] private bool isSlope;
    [Header("跳跃")]
    [Tooltip("角色跳跃的高度")] public float jumpHeight = 2.5f;
    [Tooltip("判断是否在跳跃")] private bool isJumping;
    [Header("下蹲")]
    [Tooltip("下蹲时候的玩家高度")] private float crouchHeight;
    [Tooltip("判断玩家是否在下蹲")] private bool isCrouching;
    [Tooltip("正常站立时玩家高度")] private float standHeight;
    [Header("斜坡")]
    [Tooltip("走斜坡时施加的力度")] public float slopeForce = 6.0f;
    void Start()
    {
        standHeight = characterController.height;
        crouchHeight = standHeight / 2;
        cameraLocalPosition = mainCamera.localPosition;
        speed = walkSpeed;
    }
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
        //地面检测
        isGround = IsGrounded();
        //头顶检测
        isCanStand = CanStand();
        //斜坡检测
        isSlope = OnSlope();
        SetSpeed();
        SetRun();
        SetCrouch();
        SetMove();
        SetJump();
    }
    //速度设置
    void SetSpeed()
    {
        if (isRun)
        {
            speed = runSpeed;
        }
        else if (isCrouching)
        {
            speed = crouchSpeed;
        }
        else
        {
            speed = walkSpeed;
        }
    }
    //控制奔跑
    void SetRun()
    {
        if (Input.GetKey(KeyCode.LeftShift) && !isCrouching)
        {
            isRun = true;
        }
        else
        {
            isRun = false;
        }
    }
    //控制下蹲
    void SetCrouch()
    {
        if (Input.GetKey(KeyCode.LeftControl))
        {
            Crouch(true);
        }
        else
        {
            Crouch(false);
        }
    }
    //控制移动
    void SetMove()
    {
        if (isGround)
        {
            moveDirection = transform.right * horizontal + transform.forward * vertical; // 计算移动方向
            //将该向量从局部坐标系转换为世界坐标系,得到最终的移动方向
            // moveDirection = transform.TransformDirection(new Vector3(h, 0, v));
            moveDirection = moveDirection.normalized; // 归一化移动方向,避免斜向移动速度过快  
        }
    }
    //控制跳跃
    void SetJump()
    {
        if (Input.GetButtonDown("Jump") && isGround)
        {
            isJumping = true;
            moveDirection.y = jumpHeight;
        }
        moveDirection.y -= Gravity * Time.deltaTime;
        characterController.Move(moveDirection * Time.deltaTime * speed);
        //为了不影响跳跃,一定要在isJumping = false之前加力
        SetSlope();
        isJumping = false;
    }
    //控制斜坡
    public void SetSlope()
    {
        //如果处于斜坡
        if (isSlope && !isJumping)
        {
            //向下增加力
            moveDirection.y = characterController.height / 2 * slopeForceRayLength;
            characterController.Move(Vector3.down * characterController.height / 2 * slopeForce * Time.deltaTime);
        }
    }
    //newCrouching控制下蹲起立
    public void Crouch(bool newCrouching)
    {
        if (!newCrouching && !isCanStand) return; //准备起立时且头顶有东西,不能进行站立
        isCrouching = newCrouching;
        float targetHeight = isCrouching ? crouchHeight : standHeight;
        float heightChange = targetHeight - characterController.height; //计算高度变化
        characterController.height = targetHeight; //根据下蹲状态设置下蹲时候的高度和站立的高度
        characterController.center += new Vector3(0, heightChange / 2, 0); //根据高度变化调整中心位置
        // 设置下蹲站立时候的摄像机高度
        float heightTarget = isCrouching ? cameraLocalPosition.y / 2 + characterController.center.y : cameraLocalPosition.y;
        height = Mathf.Lerp(height, heightTarget, interpolationSpeed * Time.deltaTime);
        mainCamera.localPosition = new Vector3(cameraLocalPosition.x, height, cameraLocalPosition.z);
    }
    //是否可以起立,及头顶是否有物品
    bool CanStand()
    {
        Collider[] colliders = Physics.OverlapBox(headCheck.position, halfExtents);
        foreach (Collider collider in colliders)
        {
            //忽略角色自身和所有子集碰撞体
            if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform))
            {
                return false;
            }
        }
        return true;
    }
    //是否在地面
    bool IsGrounded()
    {
        Collider[] colliders = Physics.OverlapSphere(groundCheck.position, sphereRadius);
        foreach (Collider collider in colliders)
        {
            if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform)) // 忽略角色自身和所有子集碰撞体
            {
                return true;
            }
        }
        return false;
    }
    //是否在斜面
    public bool OnSlope()
    {
        RaycastHit hit;
        // 向下打出射线(检测是否在斜坡上)
        if (Physics.Raycast(transform.position + characterController.height / 2 * Vector3.down, Vector3.down, out hit, characterController.height / 2 * slopeForceRayLength))
        {
            // 如果接触到的点的法线,不在(0,1,0)的方向上,那么人物就在斜坡上
            if (hit.normal != Vector3.up)
                return true;
        }
        return false;
    }
    //判断child是否是parent的子集
    bool IsChildOf(Transform child, Transform parent)
    {
        while (child != null)
        {
            if (child == parent)
            {
                return true;
            }
            child = child.parent;
        }
        return false;
    }
    //在场景视图显示检测,方便调试
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        //头顶检测可视化
        Gizmos.DrawWireCube(headCheck.position, halfExtents * 2f);
        //地面检测可视化
        Gizmos.DrawWireSphere(groundCheck.position, sphereRadius);
        //斜坡检测可视化
        Debug.DrawRay(transform.position + characterController.height / 2 * Vector3.down, Vector3.down * characterController.height / 2 * slopeForceRayLength, Color.blue);
    }
}

补充,简单版本

using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class MovementScript : MonoBehaviour
{
    [Tooltip("角色控制器")] public CharacterController characterController;
    [Tooltip("重力加速度")] private float Gravity = -19.8f;
    private float horizontal;
    private float vertical;
    [Header("相机")]
    [Tooltip("摄像机相机")] public Transform mainCamera;
    [Tooltip("摄像机高度变化的平滑值")] public float interpolationSpeed = 10f;
    [Tooltip("当前摄像机的位置")] private Vector3 cameraLocalPosition;
    [Tooltip("当前摄像机的高度")] private float height;
    [Header("移动")]
    [Tooltip("角色行走的速度")] public float walkSpeed = 6f;
    [Tooltip("角色奔跑的速度")] public float runSpeed = 9f;
    [Tooltip("角色下蹲的速度")] public float crouchSpeed = 3f;
    [Tooltip("角色移动的方向")] private Vector3 moveDirection;
    [Tooltip("当前速度")] private float speed;
    [Tooltip("是否奔跑")] private bool isRun;
    [Header("地面检测")]
    [Tooltip("是否在地面")] private bool isGround;
    [Header("头顶检测")]
    [Tooltip("头顶检测位置")] public Transform headCheck;
    [Tooltip("盒子半长、半宽、半高")] public Vector3 halfExtents = new Vector3(0.4f, 0.5f, 0.4f);
    [Tooltip("判断玩家是否可以站立")] private bool isCanStand;
    [Header("跳跃")]
    [Tooltip("角色跳跃的高度")] public float jumpHeight = 2.5f;
    private float _verticalVelocity;
    [Header("下蹲")]
    [Tooltip("下蹲时候的玩家高度")] private float crouchHeight;
    [Tooltip("判断玩家是否在下蹲")] private bool isCrouching;
    [Tooltip("正常站立时玩家高度")] private float standHeight;
    void Start()
    {
        standHeight = characterController.height;
        crouchHeight = standHeight / 2;
        cameraLocalPosition = mainCamera.localPosition;
        speed = walkSpeed;
    }
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
        //地面检测
        isGround = characterController.isGrounded;
        //头顶检测
        isCanStand = CanStand();
        SetSpeed();
        SetRun();
        SetCrouch();
        SetMove();
        SetJump();
    }
    //速度设置
    void SetSpeed()
    {
        if (isRun)
        {
            speed = runSpeed;
        }
        else if (isCrouching)
        {
            speed = crouchSpeed;
        }
        else
        {
            speed = walkSpeed;
        }
    }
    //控制奔跑
    void SetRun()
    {
        if (Input.GetKey(KeyCode.LeftShift) && !isCrouching)
        {
            isRun = true;
        }
        else
        {
            isRun = false;
        }
    }
    //控制下蹲
    void SetCrouch()
    {
        if (Input.GetKey(KeyCode.LeftControl))
        {
            Crouch(true);
        }
        else
        {
            Crouch(false);
        }
    }
    //控制移动
    void SetMove()
    {
        moveDirection = transform.right * horizontal + transform.forward * vertical; // 计算移动方向
        //将该向量从局部坐标系转换为世界坐标系,得到最终的移动方向
        // moveDirection = transform.TransformDirection(new Vector3(h, 0, v));
        moveDirection = moveDirection.normalized; // 归一化移动方向,避免斜向移动速度过快  
    }
    //控制跳跃
    void SetJump()
    {
        bool jump = Input.GetButtonDown("Jump");
        if (isGround)
        {
            // 在着地时阻止垂直速度无限下降
            if (_verticalVelocity < 0.0f)
            {
                _verticalVelocity = -2f;
            }
            if (jump)
            {
                _verticalVelocity = jumpHeight;
            }
        }
        else
        {
            //随时间施加重力
            _verticalVelocity += Gravity * Time.deltaTime;
        }
        characterController.Move(moveDirection * speed * Time.deltaTime + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
    }
    //newCrouching控制下蹲起立
    public void Crouch(bool newCrouching)
    {
        if (!newCrouching && !isCanStand) return; //准备起立时且头顶有东西,不能进行站立
        isCrouching = newCrouching;
        float targetHeight = isCrouching ? crouchHeight : standHeight;
        float heightChange = targetHeight - characterController.height; //计算高度变化
        characterController.height = targetHeight; //根据下蹲状态设置下蹲时候的高度和站立的高度
        characterController.center += new Vector3(0, heightChange / 2, 0); //根据高度变化调整中心位置
        // 设置下蹲站立时候的摄像机高度
        float heightTarget = isCrouching ? cameraLocalPosition.y / 2 + characterController.center.y : cameraLocalPosition.y;
        height = Mathf.Lerp(height, heightTarget, interpolationSpeed * Time.deltaTime);
        mainCamera.localPosition = new Vector3(cameraLocalPosition.x, height, cameraLocalPosition.z);
    }
    //是否可以起立,及头顶是否有物品
    bool CanStand()
    {
        Collider[] colliders = Physics.OverlapBox(headCheck.position, halfExtents);
        foreach (Collider collider in colliders)
        {
            //忽略角色自身和所有子集碰撞体
            if (collider.gameObject != gameObject && !IsChildOf(collider.transform, transform))
            {
                return false;
            }
        }
        return true;
    }
    //判断child是否是parent的子集
    bool IsChildOf(Transform child, Transform parent)
    {
        while (child != null)
        {
            if (child == parent)
            {
                return true;
            }
            child = child.parent;
        }
        return false;
    }
    //在场景视图显示检测,方便调试
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        //头顶检测可视化
        Gizmos.DrawWireCube(headCheck.position, halfExtents * 2f);
    }
}

实现物理碰撞效果(2024/01/02补充)

这段代码的作用是在角色控制器与其他碰撞体发生碰撞时,给碰撞体施加一个基于角色控制器速度的冲量,以模拟碰撞的物理效果。

private CollisionFlags m_CollisionFlags; // 碰撞标记
m_CollisionFlags = characterController.Move(...); // 移动角色控制器
        
private void OnControllerColliderHit(ControllerColliderHit hit)
{
    Debug.Log("与其他碰撞体发生碰撞");
    //获取碰撞体上的 Rigidbody 组件。
    Rigidbody body = hit.collider.attachedRigidbody;
    //CollisionFlags.Below 表示角色控制器与碰撞体之间是底部接触
    if (m_CollisionFlags == CollisionFlags.Below)
    {
        return;
    }
    //然后,再次检查获取到的 Rigidbody 是否为空或者是否为静态物体(isKinematic),如果是,则同样不进行任何处理,直接返回。
    if (body == null || body.isKinematic)
    {
        return;
    }
    body.AddForceAtPosition(characterController.velocity * 0.1f, hit.point, ForceMode.Impulse); // 在碰撞点施加冲量
}

目录
相关文章
|
4月前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
194 6
|
4月前
|
图形学 机器学习/深度学习 人工智能
颠覆传统游戏开发,解锁未来娱乐新纪元:深度解析如何运用Unity引擎结合机器学习技术,打造具备自我进化能力的智能游戏角色,彻底改变你的游戏体验——从基础设置到高级应用全面指南
【8月更文挑战第31天】本文探讨了如何在Unity中利用机器学习增强游戏智能。作为领先的游戏开发引擎,Unity通过ML-Agents Toolkit等工具支持AI代理的强化学习训练,使游戏角色能自主学习完成任务。文章提供了一个迷宫游戏示例及其C#脚本,展示了环境观察、动作响应及奖励机制的设计,并介绍了如何设置训练流程。此外,还提到了Unity与其他机器学习框架(如TensorFlow和PyTorch)的集成,以实现更复杂的游戏玩法。通过这些技术,游戏的智能化程度得以显著提升,为玩家带来更丰富的体验。
65 1
|
4月前
|
开发者 图形学 Java
揭秘Unity物理引擎核心技术:从刚体动力学到关节连接,全方位教你如何在虚拟世界中重现真实物理现象——含实战代码示例与详细解析
【8月更文挑战第31天】Unity物理引擎对于游戏开发至关重要,它能够模拟真实的物理效果,如刚体运动、碰撞检测及关节连接等。通过Rigidbody和Collider组件,开发者可以轻松实现物体间的互动与碰撞。本文通过具体代码示例介绍了如何使用Unity物理引擎实现物体运动、施加力、使用关节连接以及模拟弹簧效果等功能,帮助开发者提升游戏的真实感与沉浸感。
88 1
|
3月前
|
vr&ar 图形学 API
Unity与VR控制器交互全解:从基础配置到力反馈应用,多角度提升虚拟现实游戏的真实感与沉浸体验大揭秘
【8月更文挑战第31天】虚拟现实(VR)技术迅猛发展,Unity作为主流游戏开发引擎,支持多种VR硬件并提供丰富的API,尤其在VR控制器交互设计上具备高度灵活性。本文详细介绍了如何在Unity中配置VR支持、设置控制器、实现按钮交互及力反馈,结合碰撞检测和物理引擎提升真实感,助力开发者创造沉浸式体验。
170 0
|
4月前
|
开发者 图形学 C#
深度解密:Unity游戏开发中的动画艺术——Mecanim状态机如何让游戏角色栩栩如生:从基础设置到高级状态切换的全面指南,助你打造流畅自然的游戏动画体验
【8月更文挑战第31天】Unity动画系统是游戏开发的关键部分,尤其适用于复杂角色动画。本文通过具体案例讲解Mecanim动画状态机的使用方法及原理。我们创建一个游戏角色并设计行走、奔跑和攻击动画,详细介绍动画状态机设置及脚本控制。首先导入动画资源并添加Animator组件,然后创建Animator Controller并设置状态间的转换条件。通过编写C#脚本(如PlayerMovement)控制动画状态切换,实现基于玩家输入的动画过渡。此方法不仅适用于游戏角色,还可用于任何需动态动画响应的对象,增强游戏的真实感与互动性。
105 0
|
4月前
|
开发者 图形学 Java
Unity物理引擎深度揭秘:从刚体碰撞到软体模拟,全面解析实现复杂物理交互的技巧与秘诀,助你打造超真实游戏体验
【8月更文挑战第31天】物理模拟在游戏开发中至关重要,可让虚拟世界更真实。Unity作为强大的跨平台游戏引擎,内置物理系统,支持从刚体碰撞到布料模拟的多种功能。通过添加Rigidbody组件,可实现物体受力和碰撞;使用AddForce()施加力;通过关节(如Fixed Joint)连接刚体以模拟复杂结构。Unity还支持软体物理,如布料和绳索模拟,进一步增强场景丰富度。掌握这些技术,可大幅提升游戏的真实感和玩家体验。
130 0
|
6月前
|
图形学 开发者
【Unity小技巧】unity移动物体的探究——使用8个不同方法
【Unity小技巧】unity移动物体的探究——使用8个不同方法
207 1
|
6月前
|
图形学
【unity小技巧】Unity中实现一个战斗连击连招系统,可以动态添加减少连击连招段数功能
【unity小技巧】Unity中实现一个战斗连击连招系统,可以动态添加减少连击连招段数功能
128 0
|
6月前
|
存储 图形学
【unity小技巧】unity事件系统创建通用的对象交互的功能
【unity小技巧】unity事件系统创建通用的对象交互的功能
61 0
|
6月前
|
图形学
【unity小技巧】unity通过代码进行更改后处理效果
【unity小技巧】unity通过代码进行更改后处理效果
81 0