【unity实战】Cinemachine虚拟相机+Character Controller实现俯视角、第三人称角色控制,复制粘贴即用

最终效果

文章目录

一、前言

前面其实已经做过使用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俯视角人物操作

代码

csharp 复制代码
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 方法,并手动管理重力和跳跃逻辑。

bash 复制代码
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来操作人物的旋转

csharp 复制代码
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

csharp 复制代码
public float Gravity = 19.8f;//重力
private Vector3 Velocity = Vector3.zero; // 角色的当前速度

// 更新重力影响的速度
Velocity.y += Gravity * Time.deltaTime;
// 使用角色控制器应用重力的移动
controller.Move(Velocity * Time.deltaTime);

效果

3、 加地面检测,限制在地面重力不要累加

可以看到,目前角色下落很快,那是因为目前游戏,角色重力就是一直累加,我们先加地面检测,地面检测其实有两种办法

3.1、自定义球形区域检测

你可以选择自己在角色脚下放置一个球形检测区域做地面检测,就像下面这样

csharp 复制代码
[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

csharp 复制代码
// 应用重力
controller.Move(velocity * Time.fixedDeltaTime);
//地面检测
isGround = controller.isGrounded;
if (isGround)
{
    velocity.y = -2f;
}else{
    // 重力累加
    velocity.y += Gravity * Time.fixedDeltaTime;
}

4、跳跃

其实也就是在垂直方向施加一个力,这里有一个通用的近似物理效果的一个运动公式

csharp 复制代码
// 跳跃处理
if (isGround && Input.GetButtonDown("Jump"))
{
    velocity.y = Mathf.Sqrt(JumpHeight * -2 * Gravity);
}

效果

5、物理碰撞效果

Character Controller本身不会对力作出反应,也不会自动推开刚体。

如果要通过角色控制器来推动刚体或对象,可以编写脚本通过 OnControllerColliderHit() 函数对与控制器碰撞的任何对象施力。

另一方面,如果希望玩家角色受到物理组件的影响,那么可能更适合使用rigidbody,而不是Character Controller

csharp 复制代码
 //物理碰撞
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、最终代码

csharp 复制代码
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

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

相关推荐
格林威1 小时前
Baumer工业相机堡盟工业相机如何通过BGAPISDK使用短曝光功能(曝光可设置1微秒)(C语言)
c语言·开发语言·人工智能·数码相机·计算机视觉
仰望大佬0071 小时前
HalconDotNet中的图像视频采集
数码相机·计算机视觉·c#·音视频·halcon
ItJavawfc2 小时前
Camera2 预览旋转方向、拍照、录像成像旋转
数码相机·camera2
格林威2 小时前
Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的图像剪切(ROI)功能(C语言)
c语言·开发语言·人工智能·数码相机·计算机视觉
charon87783 小时前
虚幻引擎 | (类恐鬼症)玩家和NPC语音聊天(中)
游戏引擎·虚幻
嗡嗡嗡qwq8 小时前
海康威视相机在QTcreate上的环境配置教程(qt+opencv+海康SDK)
数码相机·qt·opencv
yqssjhf10 小时前
爱普生相机SD卡格式化后数据恢复指南
数码相机·sd卡·sd卡数据恢复·爱普生相机
&岁月不待人&12 小时前
Android 通过相机和系统相册获取图片,压缩,结果回调
android·数码相机
z2014z12 小时前
Unity Timeline
unity·游戏引擎
tealcwu12 小时前
【Unity基础】如何选择Mono的.Net API版本
unity·游戏引擎·.net