最终效果

 
文章目录
- 最终效果
 - 一、前言
 - 二、Character Controller参数介绍
 - 三、添加虚拟相机
 - 四、2.5D俯视角人物操作
 - 五、自带重力的SimpleMove 移动
 - 六、第三人称角色控制
 - 1、移动
 - 2、添加重力
 - 3、 加地面检测,限制在地面重力不要累加
 - 3.1、自定义球形区域检测
 - 3.2、使用isGrounded判断是否接触地面
 
- 4、跳跃
 - 5、物理碰撞效果
 - 6、最终代码
 
- 七、下蹲
 - 完结
 
一、前言
前面其实已经做过使用CharacterController实现过第一人称的角色控制器:
 【unity小技巧】unity最完美的CharacterController 3d角色控制器,实现移动、跳跃、下蹲、奔跑、上下坡、物理碰撞效果,复制粘贴即用
但是我发现很多人还是不理解,都跑来私信问我,所以决定重新做一个更加详细且简单控制器,主要是实现俯视角和第三人称角色控制。
二、Character Controller参数介绍
添加一个胶囊体,添加Character Controller组件,记得删除Capsule Collider组件,因为Character Controller自带了碰撞,所以不需要
 
 Character Controller参数介绍
 
Slope Limit 可以爬坡的角度
Step Offset 能上的楼梯阶梯高度
skin width 相当于在CharacterController自己的碰撞体外再添加了一个与碰撞角度相关的碰撞体,skinwith可以消除抖动并且防止角色卡住。这里的建议是最好让你的skinwith的值至少大于0.01并且大于控制器半径的10%,但是调整了这个值可能会导致角色的脚无法触及地面,记得同时把碰撞体重心的位置向上调整一点点就可以了
Min Move Distance 最小的移动距离是指的是低于这个数值的移动会被系统忽略掉,我们一般设置为0
Center、Radius、Height其实就是定义碰撞体范围,中心位置、半径、高
三、添加虚拟相机
参考:【推荐100个unity插件之10】Unity最全的最详细的Cinemachine(虚拟相机系统)介绍,详细案例讲解,快速上手
四、2.5D俯视角人物操作
代码
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
    private CharacterController controller; // 角色控制器组件的引用
	public float Speed = 0.1f; // 玩家移动的速度
	float horizontal;
    float vertical;
    
	void Start()
    {
        controller = GetComponent<CharacterController>(); // 初始化角色控制器
    }
    
	void Update()
    {
        // 获取水平和垂直轴的输入
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
		
		MoveLikeTopDown();
    }
    private void MoveLikeTopDown()
    {
        // 获取水平和垂直轴的输入
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        // 根据输入创建移动方向向量,并标准化
        Vector3 direction = new Vector3(horizontal, 0, vertical).normalized;
        // 计算移动向量并根据速度和时间更新位置
        Vector3 move = direction * Speed * Time.deltaTime;
        controller.Move(move);
        // 将玩家的位置从世界坐标转换为屏幕坐标
        Vector3 playerScreenPoint = Camera.main.WorldToScreenPoint(transform.position);
        // 计算鼠标相对于玩家的偏移量
        Vector3 point = Input.mousePosition - playerScreenPoint;
        // 计算与鼠标位置的角度
        float angle = Mathf.Atan2(point.x, point.y) * Mathf.Rad2Deg;
        // 更新玩家的朝向,使其面向鼠标
        transform.eulerAngles = new Vector3(transform.eulerAngles.x, angle, transform.eulerAngles.z);
    }
}
 
配置
 
相机参数改一下,改成俯视角,拉高相机
 
 效果
 
五、自带重力的SimpleMove 移动
SimpleMove 方法是 CharacterController 组件提供的一个简化移动方式,它会自动处理重力,并且更适合不需要手动处理重力的场景。使用 SimpleMove 方法可以让代码更简洁,因为它自动处理了重力的应用。相比之下,Move 方法则要求你手动计算和应用重力。因此,如果你的角色只需要基本的移动而不需要额外的物理处理,SimpleMove 是一个更方便的选择。
但是SimpleMove 方法本身不支持跳跃。SimpleMove 自动处理重力,但不提供直接的方式来控制跳跃或其他复杂的运动行为。如果你需要实现跳跃功能,你应该使用 Move 方法,并手动管理重力和跳跃逻辑。
void MoveLikeWow()
{
    float horizontal = Input.GetAxis("Horizontal");
    float vertical = Input.GetAxis("Vertical");
    Vector3 move = transform.forward * Speed * vertical;
    // 使用 SimpleMove 方法,它会自动处理重力
    controller.SimpleMove(move);
    // 旋转角色
    transform.Rotate(Vector3.up, horizontal * RotateSpeed);
}
 
效果,可以看到角色已经有了重力
 
六、第三人称角色控制
如前面所说,如果你的游戏不需要角色进行跳跃且对重力定制性不高功能,则使用前面的SimpleMove确实是一种很好的方法。
 如果我们角色需要跳跃等复杂的功能时,
1、移动
ws控制人物前后移动,用A和D来操作人物的旋转
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
    private CharacterController controller; // 角色控制器组件的引用
    public float Speed = 0.1f; // 玩家移动的速度
    public float RotateSpeed = 1f; // 玩家旋转的速度
    float horizontal;
    float vertical;
    void Start()
    {
        controller = GetComponent<CharacterController>(); // 初始化角色控制器
    }
    void Update()
    {
        // 获取水平和垂直轴的输入
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
		MoveLikeWow();
    }
    private void MoveLikeWow()
    {
        // 根据输入和速度计算移动方向
        Vector3 move = transform.forward * Speed * vertical;
        
        // 使用角色控制器移动角色
        controller.Move(move);
        // 根据水平输入旋转角色
        transform.Rotate(Vector3.up, horizontal * RotateSpeed);
    }
}
 
绑定相机跟随看向主角,并修改相机视角看向角色的头部
 
效果
 
相机拐弯有点抖动问题,把这个水平方向的这个震荡值调成零
 
效果
 
2、添加重力
注意controller.move调用多次的话,这里面的位移是可以互相叠加的,所以就是最终就是水平位移加上重力,不用担心下面move的会覆盖前面的move
public float Gravity = 19.8f;//重力
private Vector3 Velocity = Vector3.zero; // 角色的当前速度
// 更新重力影响的速度
Velocity.y += Gravity * Time.deltaTime;
// 使用角色控制器应用重力的移动
controller.Move(Velocity * Time.deltaTime);
 
效果
 
3、 加地面检测,限制在地面重力不要累加
可以看到,目前角色下落很快,那是因为目前游戏,角色重力就是一直累加,我们先加地面检测,地面检测其实有两种办法
3.1、自定义球形区域检测
你可以选择自己在角色脚下放置一个球形检测区域做地面检测,就像下面这样
[Header("地面检测")]
public bool IsGround;
public Transform GroundCheck;
public float CheckRadius = 0.2f;
public LayerMask layerMask;
//Physics.CheckSphere 用于检测一个球形区域是否与指定的碰撞层发生接触
IsGround = Physics.CheckSphere(GroundCheck.position, CheckRadius, layerMask);
 
但是这样通常会有一个问题,就是当角色处于悬崖边缘时,检测很容易不准确,导致跳不起来。
 当然你可以旋转不使用球形检测,改用其他的检测进行优化,但是这无疑会增加很多的工作量。
3.2、使用isGrounded判断是否接触地面
CharactorController自带有isGrounded函数可以判断是否接触地面
 
 但是它可不像官方文档说的这么简单,如果不懂如何正常使用的话,它存在很多坑,比如明明角色在地面上但是"CharacterController.isGrounded的值总是为false",或者"CharacterController.isGrounded的值在true和false反复横跳"。
如果你查看CharacterController.isGrounded会发现有一个段描述如下
 
 其实际意思是上一次调用CharacterController.Move时CharacterController是否接触到了地面?
所以我们可以总结出isGrounded正确进行地面检测的两个条件:
CharacterController.isGrounded的地面检测判断之前一定要先执行CharacterController.Move方法你必须一直为它施加一个向下的速度,且速度不能为0
代码实现如下,注意当isGrounded为true时,将Y轴方向的速度设为了一个较小的负值(这里我设置的-2),这是为了确保能稳定触碰地面,避免isGrounded误判为false
// 应用重力
controller.Move(velocity * Time.fixedDeltaTime);
//地面检测
isGround = controller.isGrounded;
if (isGround)
{
    velocity.y = -2f;
}else{
    // 重力累加
    velocity.y += Gravity * Time.fixedDeltaTime;
}
 
4、跳跃
其实也就是在垂直方向施加一个力,这里有一个通用的近似物理效果的一个运动公式
 
// 跳跃处理
if (isGround && Input.GetButtonDown("Jump"))
{
    velocity.y = Mathf.Sqrt(JumpHeight * -2 * Gravity);
}
 
效果
 
5、物理碰撞效果
Character Controller本身不会对力作出反应,也不会自动推开刚体。
如果要通过角色控制器来推动刚体或对象,可以编写脚本通过 OnControllerColliderHit() 函数对与控制器碰撞的任何对象施力。
另一方面,如果希望玩家角色受到物理组件的影响,那么可能更适合使用rigidbody,而不是Character Controller
 //物理碰撞
private void OnControllerColliderHit(ControllerColliderHit hit)
{
    //获取碰撞体上的 Rigidbody 组件。
    Rigidbody body = hit.rigidbody;
    
    //检查获取到的 Rigidbody 是否为空或者是否为静态物体(isKinematic)
    if (body == null || body.isKinematic) return;
    // 我们不想把物体推到我们下面 
    if (hit.moveDirection.y < -0.3) return;
    //根据移动方向计算推动方向,
    //我们只将物体推向两侧,从不上下 
    Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
    // 在碰撞点施加冲量 ForceMode.Impulse:添加一个瞬间的冲击力到刚体,且自动应用它的质量
    body.AddForceAtPosition(pushDir * 0.1f, hit.point, ForceMode.Impulse);
}
 
效果
 
6、最终代码
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
    private CharacterController characterController;
    float horizontal;
    float vertical;
    
    [Header("移动")]
    public float Speed = 10f; // 玩家移动的速度
    public Vector3 velocity = Vector3.zero; // 角色的当前速度
    [Header("旋转")]
    public float RotateSpeed = 30f; // 玩家旋转的速度
    [Header("跳跃")]
    public float JumpHeight = 3f;//跳跃高度
    public float Gravity = -39.8f;//重力
    [Header("地面检测")]
    public bool isGround;
    void Start()
    {
        characterController = GetComponent<CharacterController>(); // 初始化角色控制器
    }
    void Update()
    {
        // 获取输入
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
        // 处理角色旋转
        if (horizontal != 0)
        {
            transform.Rotate(Vector3.up * horizontal * RotateSpeed * Time.deltaTime);
        }
        // 处理角色移动
        Vector3 move = transform.forward * Speed * vertical;
        m_CollisionFlags = characterController.Move(move * Time.deltaTime);
        // 跳跃处理
        if (isGround && Input.GetButtonDown("Jump"))
        {
            velocity.y = Mathf.Sqrt(JumpHeight * -2 * Gravity);
        }
        // 应用重力
        characterController.Move(velocity * Time.deltaTime);
        //地面检测
        isGround = characterController.isGrounded;
        if (isGround)
        {
            // 当isGrounded为true时,将Y轴方向的速度设为了一个较小的负值,这是为了确保能稳定触碰地面,避免isGrounded误判为false
            velocity.y = -2f;
        }
        else
        {
            // 更新重力影响的速度
            velocity.y += Gravity * Time.deltaTime;
        }
    }
    //物理碰撞
    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        //获取碰撞体上的 Rigidbody 组件。
        Rigidbody body = hit.rigidbody;
        
        //检查获取到的 Rigidbody 是否为空或者是否为静态物体(isKinematic)
        if (body == null || body.isKinematic) return;
        // 我们不想把物体推到我们下面 
        if (hit.moveDirection.y < -0.3) return;
        //根据移动方向计算推动方向,
        //我们只将物体推向两侧,从不上下 
        Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
        // 在碰撞点施加冲量 ForceMode.Impulse:添加一个瞬间的冲击力到刚体,且自动应用它的质量
        body.AddForceAtPosition(pushDir * 0.1f, hit.point, ForceMode.Impulse);
    }
}
 
七、下蹲
下蹲的逻辑就是让CharacterController 的高度减半,还有中心点的位置也跟着减半,当然还有摄像机的高度,还需要注意的是人物如果头顶有东西的时候我们是不允许他起立的,不然会穿模,所以还需要一个头顶检测,头顶我们使用盒子检测最好,可以覆盖整个头部
参考:https://blog.csdn.net/qq_36303853/article/details/134984516
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
好了,我是向宇,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
 



















