【Unity手把手教程】一、第一/三人称控制器

系列文章目录



前言

本文将带你从零开始实现一个第一/三人称控制器,包括相机视角旋转,拉近拉远,缓动效果,相机避障,以及基础的角色控制。


一、新建项目

首先打开Unity Hub,点击右上角蓝色的新建按钮,在弹出来的窗口中选择“3D”模板,然后输入项目名称,选择项目位置,然后点击右下角蓝色创建按钮。这里我使用的Unity是2019.3.0f6版本,Unity Hub是2.4.11版本。(无需勾选启用xxx的复选框)
新建项目
然后等待Unity启动。

二、搭建一个简单的场景

unity启动好后我们可以看到一个空项目。
空项目
我们先创建一个地面。
右键层级面板中的空白位置——3D对象——平面
在这里插入图片描述
然后创建主角,这里使用自带的胶囊
右键层级面板中的空白位置——3D对象——胶囊
在这里插入图片描述
然后鼠标拖动绿色的Y轴将胶囊挪到地面以上。
在这里插入图片描述
在层级面板里的胶囊上右键——3D对象——立方体
在胶囊节点下创建一个立方体
在这里插入图片描述
在检查器中将立方体的Transform组件下的位置的z值改为1,将立方体移到胶囊的前面,方便我们观察模型的转向。当然你也可以适当调整立方体的缩放,使其美观一点。
在这里插入图片描述
右键层级面板中的空白位置——创建空对象
创建一个空对象并将其重命名为“Player”,当然不进行重命名也是可以的。
在这里插入图片描述
然后在层级面板中将胶囊节点拖到Player节点下
在这里插入图片描述
一个简单的场景就搭建好了,下面我们正式开始制作第一/三人称控制器。

三、搭建相机结构

在Player节点下创建一个空对象子节点,并重命名为“h”,用来控制相机的横向旋转。
在这里插入图片描述
在h节点下创建一个空对象子节点,并重命名为“v”,用来控制相机的纵向旋转。在这里插入图片描述
然后在层级面板中将场景里自带的Main Camera拖到v节点下。
在这里插入图片描述
适当修改h节点的Transform组件下的位置的y值,使相机注视胶囊的中上部。
至此,相机结构就搭建好了,接下来,我们将进行代码的编写。在实际环境中,你可以将胶囊节点替换为自己的模型,不过需要注意的是,胶囊是自带碰撞盒的,换成自己的模型需要手动添加碰撞盒组件。

这样做的好处

  1. 通过h和v两个节点分别控制相机横向和纵向的旋转,可以有效地避免万向节死锁。
  2. 相机的localPosition的z值表示的就是相机的远近,可以通过调整该值达到拉近/拉远相机的效果。
  3. 相机的localPosition的x值表示的就是相机左右偏移量,可以通过调整该值达到相机偏移的效果。
  4. 相机偏移量为0时,将注视h节点,可以通过调整h节点的localPosition的y值达到调整相机高度的效果。通常人物模型的高度不是固定的,因此此效果将非常有效。

四、编写代码

在正式编写代码之前,我们先将要进行的工作整理为一个列表。

  • 锁定鼠标
  • 相机跟随鼠标移动旋转
  • 整体跟随相机方向移动
  • 模型跟随相机方向和移动方向旋转
  • 相机拉近/拉远
  • 相机避障

1.锁定鼠标

将下面的项目/控制台面板切换到项目标签。
右键空白区域——创建——文件夹,重命名为“Script”,用来存放我们即将编写的脚本。
双击进入Script文件夹。
右键空白区域——创建——C#脚本,重命名为“CursorLockHide”,意为鼠标锁定隐藏。
在这里插入图片描述
双击该文件,打开Visual Studio进行编辑。
我们需要实现的效果是,进入游戏自动锁定并隐藏鼠标,按住左alt键显示鼠标,松开左alt键再次锁定并隐藏鼠标。
所以我们在Start方法中输入以下代码:

Cursor.lockState = CursorLockMode.Locked;

这样即可实现进入游戏自动锁定并隐藏鼠标。
你也可以将你的Start方法改写成这样:

void Start() => Cursor.lockState = CursorLockMode.Locked;

下面我们来实现按住左alt键显示鼠标,松开左alt键再次锁定并隐藏鼠标的效果。
在Update方法中输入以下代码:

        if(Input.GetKeyDown(KeyCode.LeftAlt))
        {
            Cursor.lockState = CursorLockMode.None;
        } 
        else if(Input.GetKeyUp(KeyCode.LeftAlt)) 
        {
            Cursor.lockState = CursorLockMode.Locked;
        }

这样即可实现按住左alt键显示鼠标,松开左alt键再次锁定并隐藏鼠标的效果。
现在,我们可以将这个脚本拖到Player节点上,并点击运行测试一下。
完整的CursorLockHide脚本如下:

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

public class CursorLockHide : MonoBehaviour
{
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftAlt))
        {
            Cursor.lockState = CursorLockMode.None;
        }
        else if (Input.GetKeyUp(KeyCode.LeftAlt))
        {
            Cursor.lockState = CursorLockMode.Locked;
        }
    }
}

2.相机跟随鼠标移动旋转

右键空白区域——创建——C#脚本,重命名为“CameraCtrl”,意为相机控制。
之前讲到,我们的相机旋转是通过控制h节点和v节点的旋转实现的,所以我们要先拿到这两个节点,我们将该脚本拖到Main Camera上,并双击编辑脚本。
要获取h节点和v节点,需要在Start方法前输入以下代码:

    [Header("横向节点")]
    public GameObject hNode;
    [Header("纵向节点")]
    public GameObject vNode;

然后回到Unity中,点击Main Camera,将h节点和v节点拖到右侧检查器面板上Camera Ctrl脚本组件上的对应位置。在这里插入图片描述
这样我们就可以在脚本中通过代码的方式控制h节点和v节点了。
我们继续向Start方法前添加代码:

    [Header("环绕速度")]
    public float xSpeed = 0.01f;
    [Header("垂直速度")]
    public float ySpeed = 0.01f;
    [Header("X向旋转")]
    public float rot_x = 0f;
    [Header("Y向旋转")]
    public float rot_y = 0f;

xSpeed代表横向旋转的速度,即环绕速度;ySpeed代表纵向旋转的速度,即垂直速度。
rot_x和rot_y代表了相机的初始旋转。
接着,我们向Start方法中添加代码:

        // 初始化节点旋转
        hNode.transform.localRotation = Quaternion.identity;
        vNode.transform.localRotation = Quaternion.identity;
        // 先横向旋转
        hNode.transform.Rotate(0, rot_x, 0);
        // 再纵向旋转
        vNode.transform.Rotate(rot_y, 0, 0);

这样相机将在进入游戏时按rot_x和rot_y进行旋转。
我们希望当鼠标为显示状态的时候,即左alt键按下的时候不进行对相机的控制,所以我们在Start方法前增加一个变量:

    private bool showMouse = false;

并在Update方法中添加以下代码:

        // 获得鼠标显示状态
        if (Input.GetKeyDown(KeyCode.LeftAlt))
        {
            showMouse = true;
        }
        else if (Input.GetKeyUp(KeyCode.LeftAlt))
        {
            showMouse = false;
        }

然后,我们在Update后添加一个方法FixedUpdate:

    void FixedUpdate()
    {

    }

这个方法将以固定时间间隔调用,所有对相机进行的操作都要写在这里,防止出现抖动的情况。
接下来,我们在FixedUpdate中输入以下代码:

        if (!showMouse)
        {
            // 获取鼠标输入
            float inputX = Input.GetAxis("Mouse X");
            float inputY = Input.GetAxis("Mouse Y");
            // 将鼠标输入乘以速度
            float changeX = inputX * xSpeed;
            float changeY = -inputY * ySpeed;

            // 旋转控制节点
            vNode.transform.Rotate(changeY, 0, 0);
            hNode.transform.Rotate(0, changeX, 0);
        }

现在,我们可以回到Unity中测试一下了。
你可能发现,运行游戏后鼠标的移动并不能控制相机的旋转,那是因为刚刚在代码中设置的环绕速度和垂直速度太小了,你可以在Main Camera的检查器面板中调整它们到一个合适的数值。这里,我将两个值均调成了7。
再次运行,可以看到,相机可以跟随鼠标进行360度全方位的旋转,不过,这不是我们希望看到的,因为当旋转到某个角度时将出现角色大头朝下的画面,并且此时横向的旋转与常理是反向的。因此,我们需要对相机的旋转做出一些限制。
回到CameraCtrl脚本,在Start方法前添加以下代码:

    [Header("Y向旋转上限")]
    public float rotYMax = 70f;
    [Header("Y向旋转下限")]
    public float rotYMin = 290f;

我们希望通过这两个值来限制垂直方向的旋转。
接着,在FixedUpdate方法中旋转控制节点前添加以下代码:

            // 获取v节点的本地旋转
            Vector3 vRotation = vNode.transform.localEulerAngles;
            // 限制相机视角上下旋转
            if (vRotation.x < 180 && vRotation.x + changeY > rotYMax)
            {
                changeY = rotYMax - vRotation.x;
            }
            else if (vRotation.x > 180 && vRotation.x + changeY < rotYMin)
            {
                changeY = rotYMin - vRotation.x;
            }

回到Unity,再次运行,可以看到相机的垂直旋转已经被限制在一个合适的范围了,同样,你可以在Main Camera的检查器面板中调整这个范围。
至此,我们已经实现了相机跟随鼠标移动进行旋转的功能。
完整的CameraCtrl脚本如下:

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

public class CameraCtrl : MonoBehaviour
{
    [Header("横向节点")]
    public GameObject hNode;
    [Header("纵向节点")]
    public GameObject vNode;
    [Header("环绕速度")]
    public float xSpeed = 0.01f;
    [Header("垂直速度")]
    public float ySpeed = 0.01f;
    [Header("X向旋转")]
    public float rot_x = 0f;
    [Header("Y向旋转")]
    public float rot_y = 0f;
    [Header("Y向旋转上限")]
    public float rotYMax = 70f;
    [Header("Y向旋转下限")]
    public float rotYMin = 290f;

    private bool showMouse = false;


    void Start()
    {
        // 初始化节点旋转
        hNode.transform.localRotation = Quaternion.identity;
        vNode.transform.localRotation = Quaternion.identity;
        // 先横向旋转
        hNode.transform.Rotate(0, rot_x, 0);
        // 再纵向旋转
        vNode.transform.Rotate(rot_y, 0, 0);

    }

    void Update()
    {
        // 获得鼠标显示状态
        if (Input.GetKeyDown(KeyCode.LeftAlt))
        {
            showMouse = true;
        }
        else if (Input.GetKeyUp(KeyCode.LeftAlt))
        {
            showMouse = false;
        }
    }

    void FixedUpdate()
    {
        if (!showMouse)
        {
            // 获取鼠标输入
            float inputX = Input.GetAxis("Mouse X");
            float inputY = Input.GetAxis("Mouse Y");
            // 将鼠标输入乘以速度
            float changeX = inputX * xSpeed;
            float changeY = -inputY * ySpeed;

            // 获取v节点的本地旋转
            Vector3 vRotation = vNode.transform.localEulerAngles;
            // 限制相机视角上下旋转
            if (vRotation.x < 180 && vRotation.x + changeY > rotYMax)
            {
                changeY = rotYMax - vRotation.x;
            }
            else if (vRotation.x > 180 && vRotation.x + changeY < rotYMin)
            {
                changeY = rotYMin - vRotation.x;
            }

            // 旋转控制节点
            vNode.transform.Rotate(changeY, 0, 0);
            hNode.transform.Rotate(0, changeX, 0);
        }
    }
}

3.整体跟随相机方向移动

新建脚本,重命名为Player。拖到Player节点上。
双击编辑脚本,在Start方法前输入以下代码:

    [Header("速度")]
    public float speed = 1;

我们希望通过这个值来控制移动速度。
接着,我们在Update方法后面输入以下代码:

    void FixedUpdate()
    {
        // 移动
        // 获取按键状态
        Vector3 translationFace = new Vector3();
        if (Input.GetKey(KeyCode.W))
        {
            translationFace.z += 1;
        }
        if (Input.GetKey(KeyCode.S))
        {
            translationFace.z -= 1;
        }
        if (Input.GetKey(KeyCode.A))
        {
            translationFace.x -= 1;
        }
        if (Input.GetKey(KeyCode.D))
        {
            translationFace.x += 1;
        }
        // 移动向量归一化
        translationFace.Normalize();

        if (translationFace.magnitude != 0)
        {
            transform.Translate(translationFace * speed, Space.Self);
        }

    }

我们通过检测wasd四个按键的持续按下事件获得一个位移矢量,并对其进行归一化,来使当玩家同时按下两个按键时的实际速度与按下一个按键时的实际速度相同,然后我们通过Translate函数对Player进行移动。
回到Unity测试,可以看到,当我们按下wasd时,Player发生了移动,不过,这个移动并不是按相机方向进行的,还没有达到我们的预期效果。
回到Player脚本,在Start方法前添加以下代码:

    [Header("位移矢量偏移跟随节点")]
    public GameObject hNode;

然后回到编辑器,将h节点拖到这里。
接着,在FixedUpdate方法中移动向量归一化的后面添加以下代码:

        // 将移动向量的坐标系由世界坐标系向hNode节点的坐标系转变
        translationFace = Quaternion.AngleAxis(hNode.transform.localEulerAngles.y, Vector3.up) * translationFace;

再次回到Unity进行测试,可以看到Player的运动已经跟随相机方向了。不过速度有些快,可以在检查器面板做适当调整。
完整的Player脚本如下:

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

public class Player : MonoBehaviour
{
    [Header("速度")]
    public float speed = 1;

    [Header("位移矢量偏移跟随节点")]
    public GameObject hNode;

    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    void FixedUpdate()
    {
        // 移动
        // 获取按键状态
        Vector3 translationFace = new Vector3();
        if (Input.GetKey(KeyCode.W))
        {
            translationFace.z += 1;
        }
        if (Input.GetKey(KeyCode.S))
        {
            translationFace.z -= 1;
        }
        if (Input.GetKey(KeyCode.A))
        {
            translationFace.x -= 1;
        }
        if (Input.GetKey(KeyCode.D))
        {
            translationFace.x += 1;
        }
        // 移动向量归一化
        translationFace.Normalize();
        // 将移动向量的坐标系由世界坐标系向hNode节点的坐标系转变
        translationFace = Quaternion.AngleAxis(hNode.transform.localEulerAngles.y, Vector3.up) * translationFace;
        if (translationFace.magnitude != 0)
        {
            transform.Translate(translationFace * speed, Space.Self);
        }

    }
}

4.模型跟随相机方向和移动方向旋转

新建脚本并重命名为ModelCtrl,意为模型控制,拖到胶囊节点上。
双击编辑脚本,在Start方法前添加以下代码:

    [Header("模型旋转跟随节点")]
    public GameObject hNode;

回到Unity,将h节点拖到这里。
接着,我们在Update方法中添加以下代码:

        // 获取按键状态
        Vector3 translationFace = new Vector3();
        if (Input.GetKey(KeyCode.W))
        {
            translationFace.z += 1;
        }
        if (Input.GetKey(KeyCode.S))
        {
            translationFace.z -= 1;
        }
        if (Input.GetKey(KeyCode.A))
        {
            translationFace.x -= 1;
        }
        if (Input.GetKey(KeyCode.D))
        {
            translationFace.x += 1;
        }

        if (translationFace.magnitude != 0)
        {
            Quaternion rotateTo = hNode.transform.rotation;
            rotateTo *= Quaternion.LookRotation(translationFace, Vector3.up);

            transform.rotation = rotateTo;

        }

首先获取了wasd的按键状态,如果有任意按键按下,则获取h节点的旋转角度,然后将其乘上按键状态对应的角度,这里进行的是四元数乘法,四元数表示的是旋转角度,对其进行乘运算将得到将其旋转乘数的角度后得到的以四元数形式表示的角度。
最后将值赋给transform.rotation,进行旋转。
回到Unity进行测试,现在模型已经可以跟随相机方向和按键方向进行旋转了。
不过这种旋转是瞬时完成的,显得非常僵硬,我们可以将它做的更好。
回到ModelCtrl脚本,在Start方法前加入以下代码:

    [Header("跟随速度")]
    public float speed = 550;

接着将transform.rotation = rotateTo;这句代码改成以下内容:

            transform.rotation = Quaternion.RotateTowards(transform.rotation, rotateTo, speed * Time.deltaTime);

这是四元数的缓动旋转方法,我们再次运行测试,可以看到现在模型的旋转已经从瞬间变化改为逐渐旋转过来了,不再那么僵硬了。
完整的ModelCtrl脚本如下:

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

public class ModelCtrl : MonoBehaviour
{
    [Header("模型旋转跟随节点")]
    public GameObject hNode;
    [Header("跟随速度")]
    public float speed = 550;

    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        // 获取按键状态
        Vector3 translationFace = new Vector3();
        if (Input.GetKey(KeyCode.W))
        {
            translationFace.z += 1;
        }
        if (Input.GetKey(KeyCode.S))
        {
            translationFace.z -= 1;
        }
        if (Input.GetKey(KeyCode.A))
        {
            translationFace.x -= 1;
        }
        if (Input.GetKey(KeyCode.D))
        {
            translationFace.x += 1;
        }

        if (translationFace.magnitude != 0)
        {
            Quaternion rotateTo = hNode.transform.rotation;
            rotateTo *= Quaternion.LookRotation(translationFace, Vector3.up);

            transform.rotation = Quaternion.RotateTowards(transform.rotation, rotateTo, speed * Time.deltaTime);

        }
    }
}

5.相机拉近/拉远

前面提到,Main Camera的localPosition的z值即为相机距离,所以我们获取鼠标滚轮的值并对其调整即可实现拉近/拉远效果。
我们回到CameraCtrl脚本,在Start方法前添加以下代码:

    [Header("当前距离")]
    public float dis = 5f;
    [Header("最近距离")]
    public float minDis = 1f;
    [Header("最远距离")]
    public float maxDis = 20f;
    [Header("距离改变速度")]
    public float disSpeed = 5f;
    // 用于相机距离赋值
    private Vector3 disV3;

然后在FixedUpdate方法中旋转控制节点后添加以下代码:

            // 获取滚轮长度
            float ddis = Input.GetAxis("Mouse ScrollWheel") * disSpeed;
            if (ddis != 0)
            {
                dis -= ddis;
                if (dis < minDis)
                {
                    dis = minDis;
                }
                else if (dis > maxDis)
                {
                    dis = maxDis;
                }
            }

            disV3.z = -dis;
            
            // lerp是缓动方法
            transform.localPosition = Vector3.Lerp(transform.localPosition, disV3, 0.1f);

回到Unity进行测试,可以看到当滑动滚轮时,相机进行了拉近/拉远,并且具有比较顺滑的缓动效果。
完整的CameraCtrl脚本如下:

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

public class CameraCtrl : MonoBehaviour
{
    [Header("横向节点")]
    public GameObject hNode;
    [Header("纵向节点")]
    public GameObject vNode;
    [Header("环绕速度")]
    public float xSpeed = 0.01f;
    [Header("垂直速度")]
    public float ySpeed = 0.01f;
    [Header("X向旋转")]
    public float rot_x = 0f;
    [Header("Y向旋转")]
    public float rot_y = 0f;
    [Header("Y向旋转上限")]
    public float rotYMax = 70f;
    [Header("Y向旋转下限")]
    public float rotYMin = 290f;
    [Header("当前距离")]
    public float dis = 5f;
    [Header("最近距离")]
    public float minDis = 1f;
    [Header("最远距离")]
    public float maxDis = 20f;
    [Header("距离改变速度")]
    public float disSpeed = 5f;

    private bool showMouse = false;

    // 用于相机距离赋值
    private Vector3 disV3;

    void Start()
    {
        // 初始化节点旋转
        hNode.transform.localRotation = Quaternion.identity;
        vNode.transform.localRotation = Quaternion.identity;
        // 先横向旋转
        hNode.transform.Rotate(0, rot_x, 0);
        // 再纵向旋转
        vNode.transform.Rotate(rot_y, 0, 0);

    }

    void Update()
    {
        // 获得鼠标显示状态
        if (Input.GetKeyDown(KeyCode.LeftAlt))
        {
            showMouse = true;
        }
        else if (Input.GetKeyUp(KeyCode.LeftAlt))
        {
            showMouse = false;
        }
    }

    void FixedUpdate()
    {
        if (!showMouse)
        {
            // 获取鼠标输入
            float inputX = Input.GetAxis("Mouse X");
            float inputY = Input.GetAxis("Mouse Y");
            // 将鼠标输入乘以速度
            float changeX = inputX * xSpeed;
            float changeY = -inputY * ySpeed;

            // 获取v节点的本地旋转
            Vector3 vRotation = vNode.transform.localEulerAngles;
            // 限制相机视角上下旋转
            if (vRotation.x < 180 && vRotation.x + changeY > rotYMax)
            {
                changeY = rotYMax - vRotation.x;
            }
            else if (vRotation.x > 180 && vRotation.x + changeY < rotYMin)
            {
                changeY = rotYMin - vRotation.x;
            }

            // 旋转控制节点
            vNode.transform.Rotate(changeY, 0, 0);
            hNode.transform.Rotate(0, changeX, 0);

            // 获取滚轮长度
            float ddis = Input.GetAxis("Mouse ScrollWheel") * disSpeed;
            if (ddis != 0)
            {
                dis -= ddis;
                if (dis < minDis)
                {
                    dis = minDis;
                }
                else if (dis > maxDis)
                {
                    dis = maxDis;
                }
            }

            disV3.z = -dis;
            
            // lerp是缓动方法
            transform.localPosition = Vector3.Lerp(transform.localPosition, disV3, 0.1f);

        }
    }
}

顺带一提,如果你只是想实现一个第一人称控制器,那么相机的部分看到这里就可以了,因为只要将z值改为0,就变成第一人称摄像机了。

6.相机避障

在很多时候,我们不希望相机和角色之间被其他东西阻挡,因为这很可能会造成相机穿模,所以我们需要实现相机避障的功能。这里我们通过从角色向相机应该在的位置发射一条射线来判断他们中间是否有障碍物,如果有障碍物,则调整相机的距离,使相机位于障碍物之前。
首先,在CameraCtrl脚本的Start方法前添加以下代码:

    [Header("相机碰撞距离补偿")]
    public float disFix = 0.5f;
    // 预测相机位置
    private Vector3 PredictCameraPosition;
    // 碰撞位置
    private Vector3 wallHit;

因为如果只是单纯的把相机移到碰撞点上,那么很有可能因为相机的旋转角度而看到障碍物后面的东西,所以这里引入了相机碰撞距离补偿变量,用以将相机挪到碰撞点更前一点的位置。
接着,我们在FixedUpdate方法后添加以下代码:

    bool Inwall()
    {
        RaycastHit hit;
        //将物体的Layer设置为Ignore Raycast,Player和Mob来忽略相机的射线,不然相机将跳到某些物体前,比如怪物,玩家等,
        LayerMask mask = (1 << LayerMask.NameToLayer("Player")) | (1 << LayerMask.NameToLayer("Mob")) | (1 << LayerMask.NameToLayer("Weapon"));
        //将以上的mask取反,表示射线将会忽略以上的层
        mask = ~mask;
        //预测的相机位置
        PredictCameraPosition = hNode.transform.position + (transform.position - hNode.transform.position).normalized * dis;
        //碰撞到任意碰撞体,注意,因为相机没有碰撞器,所以是不会碰撞到相机的,也就是没有碰撞物时说明没有遮挡
        if (Physics.Linecast(hNode.transform.position, PredictCameraPosition, out hit, mask))
        {
            //这个if就是指被遮挡的情况
            wallHit = hit.point;//碰撞点位置
            //Debug.DrawLine(transform.position, wallHit, Color.green);
            return true;
        }
        else//没碰撞到,也就是说没有障碍物
        {
            return false;
        }
    }

这个方法将用来判断是否有障碍物,然后我们在FixedUpdate方法中相机缓动之前调用它:

            if (Inwall())
            {
                Vector3 disHit = wallHit - hNode.transform.position;
                float disChange = -disHit.magnitude - disFix;
                if (disChange > 0)
                {
                    disChange = -0.1f;
                }
                disV3.z = disChange;
            }
            else
            {
                disV3.z = -dis;
            }

接着,我们回到Unity,将Player的层改为“Player”,不然当相机在正面时将会被移到正方体之前。
运行测试,试着旋转相机,可以看到当相机被地板遮挡时,会被自动移到地板之上。
完整的CameraCtrl脚本如下:

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

public class CameraCtrl : MonoBehaviour
{
    [Header("横向节点")]
    public GameObject hNode;
    [Header("纵向节点")]
    public GameObject vNode;
    [Header("环绕速度")]
    public float xSpeed = 0.01f;
    [Header("垂直速度")]
    public float ySpeed = 0.01f;
    [Header("X向旋转")]
    public float rot_x = 0f;
    [Header("Y向旋转")]
    public float rot_y = 0f;
    [Header("Y向旋转上限")]
    public float rotYMax = 70f;
    [Header("Y向旋转下限")]
    public float rotYMin = 290f;
    [Header("当前距离")]
    public float dis = 5f;
    [Header("最近距离")]
    public float minDis = 1f;
    [Header("最远距离")]
    public float maxDis = 20f;
    [Header("距离改变速度")]
    public float disSpeed = 5f;
    [Header("相机碰撞距离补偿")]
    public float disFix = 0.5f;

    private bool showMouse = false;

    // 用于相机距离赋值
    private Vector3 disV3;
    // 预测相机位置
    private Vector3 PredictCameraPosition;
    // 碰撞位置
    private Vector3 wallHit;

    void Start()
    {
        // 初始化节点旋转
        hNode.transform.localRotation = Quaternion.identity;
        vNode.transform.localRotation = Quaternion.identity;
        // 先横向旋转
        hNode.transform.Rotate(0, rot_x, 0);
        // 再纵向旋转
        vNode.transform.Rotate(rot_y, 0, 0);

    }

    void Update()
    {
        // 获得鼠标显示状态
        if (Input.GetKeyDown(KeyCode.LeftAlt))
        {
            showMouse = true;
        }
        else if (Input.GetKeyUp(KeyCode.LeftAlt))
        {
            showMouse = false;
        }
    }

    void FixedUpdate()
    {
        if (!showMouse)
        {
            // 获取鼠标输入
            float inputX = Input.GetAxis("Mouse X");
            float inputY = Input.GetAxis("Mouse Y");
            // 将鼠标输入乘以速度
            float changeX = inputX * xSpeed;
            float changeY = -inputY * ySpeed;

            // 获取v节点的本地旋转
            Vector3 vRotation = vNode.transform.localEulerAngles;
            // 限制相机视角上下旋转
            if (vRotation.x < 180 && vRotation.x + changeY > rotYMax)
            {
                changeY = rotYMax - vRotation.x;
            }
            else if (vRotation.x > 180 && vRotation.x + changeY < rotYMin)
            {
                changeY = rotYMin - vRotation.x;
            }

            // 旋转控制节点
            vNode.transform.Rotate(changeY, 0, 0);
            hNode.transform.Rotate(0, changeX, 0);

            // 获取滚轮长度
            float ddis = Input.GetAxis("Mouse ScrollWheel") * disSpeed;
            if (ddis != 0)
            {
                dis -= ddis;
                if (dis < minDis)
                {
                    dis = minDis;
                }
                else if (dis > maxDis)
                {
                    dis = maxDis;
                }
            }
            if (Inwall())
            {
                Vector3 disHit = wallHit - hNode.transform.position;
                float disChange = -disHit.magnitude - disFix;
                if (disChange > 0)
                {
                    disChange = -0.1f;
                }
                disV3.z = disChange;
            }
            else
            {
                disV3.z = -dis;
            }

            // lerp是缓动方法
            transform.localPosition = Vector3.Lerp(transform.localPosition, disV3, 0.1f);

        }
    }

    bool Inwall()
    {
        RaycastHit hit;
        //将物体的Layer设置为Ignore Raycast,Player和Mob来忽略相机的射线,不然相机将跳到某些物体前,比如怪物,玩家等,
        LayerMask mask = (1 << LayerMask.NameToLayer("Player")) | (1 << LayerMask.NameToLayer("Mob")) | (1 << LayerMask.NameToLayer("Weapon"));
        //将以上的mask取反,表示射线将会忽略以上的层
        mask = ~mask;
        //预测的相机位置
        PredictCameraPosition = hNode.transform.position + (transform.position - hNode.transform.position).normalized * dis;
        //碰撞到任意碰撞体,注意,因为相机没有碰撞器,所以是不会碰撞到相机的,也就是没有碰撞物时说明没有遮挡
        if (Physics.Linecast(hNode.transform.position, PredictCameraPosition, out hit, mask))
        {
            //这个if就是指被遮挡的情况
            //碰撞点位置
            wallHit = hit.point;
            return true;
        }
        else
        {
            //没碰撞到,也就是说没有障碍物
            return false;
        }
    }

}

总结

以上就是今天要讲的内容,本文仅仅简单介绍了一个第一/三人称控制器的实现,还有很多不足之处,欢迎各位留言指正。
完整工程


版权声明:本文为qq_30415409原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。