这是一篇Unity3D的相关文章。
前言
第三人称摄像机最早出现于《马里奥64》,对于整个游戏行业而言,这款游戏就是3D游戏的启蒙作,在此之前,3D游戏都是固定摄像机,操作别扭。
而3D马里奥的出现则为业界带来了摄像机系统的革新,手把手教会了业界如何更好的利用摄像机系统实现更好的效果,如今,这种跟随式摄像机已经在各种游戏中随处可见。
那么,我们该如何在Unity中制作一个类似的第三人称摄像机呢?
需求分析
在正式开始之前,我们需要思考一下我们的这个摄像机控制器应该具有哪些功能:
-
当物体运动时,摄像机能够流畅地跟随物体移动
-
可以让摄像机放大或缩小(zoom)
-
可以像在第一人称一样,调整摄像机的朝向
在此基础上,我们还可以考虑加入更多细节,例如:
-
锁定敌人后,始终锁定某个点
-
当玩家进入某种状态时,锁定某个方向
再进阶一点,我们甚至可以考虑像马里奥64一样,当玩家绕着某物体跑动时,摄像机锚定在固定位置,当玩家朝上看时,摄像机靠近玩家等等
当然,这些细节和进阶玩法是本篇文章不涉及的(
前情提要
正式开始钱,书封影有如下准备
底下的蓝色看作是地板或者什么交互体,胶囊体可以替代为玩家
输入处理
首先创建一个脚本文件用于管理玩家的输入,并挂载到玩家身上
而后在脚本中添加以下三个常量字符串
public const string MouseXString = "Mouse X";
public const string MouseYString = "Mouse Y";
public const string MouseScrollString = "Mouse ScrollWheel";
在Edit->Project Settings->Input Manager中看到这些内容,我们设置的内容就是为了匹配这三个输入
而后我们创建3个属性用于获取玩家的鼠标输入:
public static float MouseXInput { get => Input.GetAxis(MouseXString); }
public static float MouseYInput { get => Input.GetAxis(MouseYString); }
public static float MouseScrollInput { get => Input.GetAxis(MouseScrollString); }
Camera Controller
前置获取
完成自定义输入处理后,我们开始构建Camera Controller脚本,并同样挂载到玩家身上
Camera Controller的工作原理是,我们通过脚本获取鼠标的输入,而后依据摄像机的当前角度对其进行重置,并让其仍然对焦我们想要跟随的物体。
所以我们首先需要在脚本中获得摄像机本身以及我们需要跟随的对象
[Header("Framing")]
[SerializeField] private Camera _camera = null;
[SerializeField] private Transform _followObjTrans = null;
而后获取摄像机面向玩家的方向以及转角
//****************** Private Variable ********************
/// <summary>
/// Cameras forward on the x,z plane.
/// </summary>
private Vector3 _planarDirec;
private Quaternion _targetRotation;
private void Start()
{
//Assign the start position for plane direction.
_planarDirec = _followObjTrans.forward;
}
光标隐藏
同时,在Start中调用CursorAPI,隐藏鼠标光标:
//Hid the mouse cursor
Cursor.lockState = CursorLockMode.Locked;
于Update中,为避免光标未隐藏时,摄像机跟随鼠标转动,我们制作一个检测器,确保每一帧都检查光标处于隐藏状态,否则停止Camera Controller
if (Cursor.lockState != CursorLockMode.Locked)
{//Prevent the camera do not rotate with the mouse if the cursor is not locked.
return;
}
处理水平旋转
我们需要获取鼠标的移动参数从而在固定平面内旋转摄像机,我们可以进一步在Update打下如下代码:
//Handle Inputs
float _mouseX = InputHandler.MouseXInput;
_planarDirec = Quaternion.Euler(0, _mouseX, 0) * _planarDirec;
而后我们尝试Debug一下,绘制出在摄像机本有坐标的基础上加上旋转的方向线:
Debug.DrawLine(_camera.transform.position, _camera.transform.position + _planarDirec, Color.red);
回到Unity编辑器中Play测试,则可发现红线随着鼠标的移动在固定的X-Z平面内旋转,证明我们成功获取了鼠标的输入,旋转计算也是正确的
于是,我们可以进一步设置_targetRotation
并应用在摄像机上
//Final target.
_targetRotation = Quaternion.LookRotation(_planarDirec);
//Apply rotation
_camera.transform.rotation = _targetRotation;
回到Unity Editor中测试,我们会发现,摄像机正确地随着鼠标的移动在定点固定平面内旋转。
处理垂直旋转
同样的,如果我们想要让摄像机能够定点上下旋转,那么我们也需要依照水平旋转的处理方式书写代码。
这里我们希望设置一个最小最大俯仰角,所以进一步定义几个新变量
[Header("Rotation")]
[SerializeField] [Range(-90,90)] private float _minVerticleAngle = -90f;
[SerializeField] [Range(-90,90)] private float _maxVerticleAngle = 90f;
private float _targetVerticleAngle;
在Update中获取鼠标的垂直移动数据
float _mouseY = InputHandler.MouseYInput;
处理_targetVerticleAngle
,使用Mathf.Clamp
限制获取的最大值和最小值
_targetVerticleAngle = -Mathf.Clamp(_targetVerticleAngle + _mouseY, _minVerticleAngle, _maxVerticleAngle);
而后将_targetRotation
修改如下:
_targetRotation = Quaternion.LookRotation(_planarDirec) * Quaternion.Euler(_targetVerticleAngle,0,0);
回到Unity Editor中进行测试,会发现我们的摄像机可以随着鼠标,上下左右移动了,如果我们在Inspector中将isRevertYAxis
设置为true,则可以同时实现Y轴反转.
摄像机放缩
处理完了旋转,接下来处理放缩
首先我们需要获得更多的一些参数:
[Header("Distance")]
//Using to define how far away the camera can be from the player object.
[SerializeField] private float _minDistance = 0f;
[SerializeField] private float _maxDistance = 10f;
//Define how far away the camera should start away from the player.
[SerializeField] private float _defaultDistance = 5f;
private Vector3 _targetPosition;
private float _targetDistance;
并在Start中初始化_targetDistance
_targetDistance = _defaultDistance;
而后我们在Update中,如同获取鼠标的XY轴输入一样,获取其滚动输入,并如法炮制设置_targetDistance
,并进一步设计_targetPosition
,并以此修改摄像机位置
float _zoom = InputHandler.MouseScrollInput;
_targetDistance = Mathf.Clamp(_targetDistance + _zoom, _minDistance, _maxDistance);
_targetPosition = _followObjTrans.position - (_targetRotation * Vector3.forward) * _targetDistance;
_camera.transform.position = _targetPosition;
回到Unity Editor点击Play,对鼠标进行滚动,我们会发现,摄像机也能随之靠近角色或远离角色,在Inspector中将isRevertScroll后,滚动造成的结果也能逆转
很好,我们整理一下代码 ,加入各轴翻转,则这部分代码逻辑完整如下:
[Header("Settings")]
public bool _invertX = false;
public bool _invertY = false;
public bool _invertScroll = false;
/// <summary>
/// Handle the inputs apply to camera.
/// </summary>
private void CameraControl()
{
//Handle Inputs
float _mouseX = InputHandler.MouseXInput;
float _mouseY = InputHandler.MouseYInput;
float _zoom = InputHandler.MouseScrollInput;
if (_invertX) { _mouseX *= -1; }
if (_invertY) { _mouseY *= -1; }
if (_invertScroll) { _zoom *= -1; }
_planarDirec = Quaternion.Euler(0, _mouseX, 0) * _planarDirec;
_targetVerticleAngle = Mathf.Clamp(_targetVerticleAngle + _mouseY, _minVerticleAngle, _maxVerticleAngle);
//Debug.DrawLine(_camera.transform.position, _camera.transform.position + _planarDirec, Color.red);
//Get target
_targetRotation = Quaternion.LookRotation(_planarDirec) * Quaternion.Euler(_targetVerticleAngle, 0, 0);
_targetDistance = Mathf.Clamp(_targetDistance + _zoom, _minDistance, _maxDistance);
_targetPosition = _followObjTrans.position - (_targetRotation * Vector3.forward) * _targetDistance;
//Apply
_camera.transform.rotation = _targetRotation;
_camera.transform.position = _targetPosition;
}
我们忘了初始化,在Start 中加入如下声明,使Start最终如下:
private void Start()
{
//Assign the start position for plane direction.
_planarDirec = _followObjTrans.forward;
//Hid the mouse cursor
Cursor.lockState = CursorLockMode.Locked;
//Caculate Targets
_targetDistance = _defaultDistance;
_targetVerticleAngle = _defaultVerticleAngle;
_targetRotation = Quaternion.LookRotation(_planarDirec) * Quaternion.Euler(_targetVerticleAngle, 0, 0);
_targetPosition = _followObjTrans.position - (_targetRotation * Vector3.forward) * _targetDistance;
}
并加入OnValidate
方法,支持实时修改
private void OnValidate()
{
_defaultDistance = Mathf.Clamp(_defaultDistance, _minDistance, _maxDistance);
_defaultVerticleAngle = Mathf.Clamp(_defaultVerticleAngle, _maxVerticleAngle, _maxVerticleAngle);
}
对焦点
上述代码很好的实现了旋转,放缩效果,但是有一个缺点,就是我们不能很好地设置摄像机对焦的中心点,,我们或许可以创建一个Player的子物体并绑定到摄像机控制器上,通过操作该子物体的位置来操作摄像机对焦点。但是我认为这样做比较麻烦,不如直接写在CameraController
内
首先声明一个V3变量,如下:
[SerializeField] private Vector3 _framing = new Vector3(0, 0, 0);
而后在上面所写的CameraControl
方法中,添加如下代码:
Vector3 _focusPosition = _followObjTrans.position + _camera.transform.TransformDirection(_framing);
将_targetPosition = _followObjTrans.position - (_targetRotation * Vector3.forward) * _targetDistance;
修改为_targetPosition = _focusPosition - (_targetRotation * Vector3.forward) * _targetDistance;
这样做的原理是相当于在原有坐标的基础上为自身加入了一个位移,给对焦点加上了一个相对于摄像机空间不变的位移(在世界坐标系中则会改变)
这样就能实现将物体的对焦点始终至于荧幕的所需位置,同时摄像机的旋转核心仍然为对焦物体。注意,我画图的时候after的focus点画错了,focus点应该在右侧camera指向位置,而原focus点则是followObj的位置
添加碰撞
现在的摄像机虽然可以随着鼠标运作,但是会穿过各种物体,导致穿模
为解决这个问题,我们可以为他添加一个碰撞体,但是这样做的话定义哪些物体不穿模,哪些物体在摄像机前半透明显示等等后续拓展可能会非常麻烦,于是我们使用3D射线RatCastAll
,通过不同的layer来区分不同效果
我们先定义如下几样所需数据,分别是用于检测摄像机周围是否有物体的检测半径,允许摄像机穿透的物体的Layer,一个列表用于储存允许摄像机穿透的物体的碰撞体
[SerializeField] private float _checkRadius = 0.2f;
[SerializeField] private LayerMask _obstructionLayers = -1;
private List<Collider> _ignoreColliders = new List<Collider>();
而后在摄像机控制方法中,写下如下代码:
float _smallestDistance = _targetDistance;
RaycastHit[] _hits = Physics.SphereCastAll(_focusPosition, _checkRadius, _targetRotation * -Vector3.forward, _targetDistance, _obstructionLayers);
if (_hits.Length != 0)
{
foreach (RaycastHit hit in _hits)
{
if (!_ignoreColliders.Contains(hit.collider))
{
if (hit.distance < _smallestDistance)
{
_smallestDistance = hit.distance;
}
}
}
}
用一个射线检测在Layer内的所有碰撞体,并以此更新摄像机距跟随物体的最短距离
而后用_smallestDistance
代替_targetDistance
回到Unity Editor就可以发现,摄像机会自动拉近与角色间的距离,避免遮挡摄像机
平滑
大幅度的位移和角度变动会使得摄像机的运动非常割裂而不丝滑,接下来我们将解决这一问题
首先声明几个变量:
[SerializeField] private float _zoomSpeed = 10f;
[SerializeField] private float _rotationSharpness = 25f;
/// <summary>
/// Smooth new postion;
/// </summary>
private Vector3 _newPosition;
/// <summary>
/// Smooth new rotation.
/// </summary>
private Quaternion _newRotation;
而后我们稍微修改一下CameraControl
方法中的一些算法
首先修改输入处理,将原
float _zoom = InputHandler.MouseScrollInput;
替换为:
float _zoom = InputHandler.MouseScrollInput * _zoomSpeed;
实现放缩的平滑控制
并在Get target后对目标位置和角度进行插值处理
//Handle Smooth
_newRotation = Quaternion.Slerp(_camera.transform.rotation, _targetRotation, _rotationSharpness * Time.deltaTime);
_newPosition = Vector3.Lerp(_camera.transform.position, _targetPosition, _positionSharpness * Time.deltaTime);
角度用弧形插值,位置用普通插值,两个插值均基于单位时间
而后将Apply的target替换为new如下
//Apply
_camera.transform.rotation = _newRotation;
_camera.transform.position = _newPosition;
那么我们就完成了平滑处理
整个方法最终如下:
/// <summary>
/// Handle the inputs apply to camera.
/// </summary>
private void CameraControl()
{
//Handle Inputs
float _mouseX = InputHandler.MouseXInput;
float _mouseY = InputHandler.MouseYInput;
float _zoom = InputHandler.MouseScrollInput * _zoomSpeed;
//Axis invert
if (_invertX) { _mouseX *= -1; }
if (_invertY) { _mouseY *= -1; }
if (_invertScroll) { _zoom *= -1; }
//Change focus point.
Vector3 _focusPosition = _followObjTrans.position + _camera.transform.TransformDirection(_framing);
//Debug.DrawLine(_camera.transform.position, _camera.transform.TransformDirection(_framing) * 10 + _camera.transform.position ,Color.blue);
_planarDirec = Quaternion.Euler(0, _mouseX, 0) * _planarDirec;
_targetVerticleAngle = Mathf.Clamp(_targetVerticleAngle + _mouseY, _minVerticleAngle, _maxVerticleAngle);
//Debug.DrawLine(_camera.transform.position, _camera.transform.position + _planarDirec, Color.red);
_targetDistance = Mathf.Clamp(_targetDistance + _zoom, _minDistance, _maxDistance);
float _smallestDistance = _targetDistance;
RaycastHit[] _hits = Physics.SphereCastAll(_focusPosition, _checkRadius, _targetRotation * -Vector3.forward, _targetDistance, _obstructionLayers);
if (_hits.Length != 0)
{
foreach (RaycastHit hit in _hits)
{
if (!_ignoreColliders.Contains(hit.collider))
{
if (hit.distance < _smallestDistance)
{
_smallestDistance = hit.distance;
}
}
}
}
_smallestDistance = Mathf.Clamp(_smallestDistance, _minDistance, _maxDistance);
//Get target
_targetRotation = Quaternion.LookRotation(_planarDirec) * Quaternion.Euler(_targetVerticleAngle, 0, 0);
_targetPosition = _focusPosition - (_targetRotation * Vector3.forward) * _smallestDistance;
//Handle Smooth
_newRotation = Quaternion.Slerp(_camera.transform.rotation, _targetRotation, _rotationSharpness * Time.deltaTime);
_newPosition = Vector3.Lerp(_camera.transform.position, _targetPosition, _positionSharpness * Time.deltaTime);
//Apply
_camera.transform.rotation = _newRotation;
_camera.transform.position = _newPosition;
Debug.DrawLine(_focusPosition, _camera.transform.position, Color.red);
}
至此我们的摄像机控制器便完成了,不过可以考虑增加细节。
文章评论