在没有引入手柄操作前,将由鼠标来控制摄像机旋转,首先我们要获取到鼠标输入。
public class PlayerInput : MonoBehaviour
{
// 摄像机控制轴
public string cameraAxisX;
public string cameraAxisY;
// 摄像机控制信号
public float cameraUp;
public float cameraRight;
// ...
void Update()
{
// 摄像机信号
cameraUp = Input.GetAxis(cameraAxisY);
cameraRight = Input.GetAxis(cameraAxisX);
// ...
}
}
老师的方案为,将摄像机挂载到游戏对象下作为子物体,同时在摄像机的上一级添加一个新对象CameraHandle作为摄像机的父物体。如果我们要旋转,就要旋转整个玩家的游戏对象,上下旋转只需要旋转CameraHandle就行了。
这是新增脚本CameraController ,挂载到Camera上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
// 灵敏度
public float horizontalSensitivity;
public float verticalSensitivity;
// 输入模块
public PlayerInput pi;
// PlayerController游戏对象
private GameObject playerHandle;
// CameraHandle游戏对象
private GameObject cameraHandle;
// 用于存储CameraHandle的欧拉角X值
private float temp_eulerX;
void Awake()
{
cameraHandle = transform.parent.gameObject;
playerHandle = cameraHandle.transform.parent.gameObject;
pi = playerHandle.GetComponent<PlayerInput>();
}
// Update is called once per frame
void Update()
{
// 左右旋转时旋转PlayerHandle
playerHandle.transform.Rotate(Vector3.up, pi.cameraRight * horizontalSensitivity * Time.deltaTime);
// 上下旋转时旋转CameraHandle
temp_eulerX -= pi.cameraUp * verticalSensitivity * Time.deltaTime;
// 限制俯仰角
temp_eulerX = Mathf.Clamp(temp_eulerX, -40, 30);
// 赋值localEulerAngles
cameraHandle.transform.localEulerAngles = new Vector3(temp_eulerX , 0, 0);
}
}
当摄像机旋转时,角色模型不要跟着旋转。办法很简单,将模型在摄像机旋转前的欧拉角保存下来,在摄像机旋转后再将之前保存的欧拉角赋值回去就行了。所以这两个操作的位置千万别弄错。
// 角色模型游戏对象
private GameObject model;
void Awake()
{
// ...
model = playerHandle.GetComponent<PlayerController>().model;
}
// Update is called once per frame
void Update()
{
// 得到摄像机旋转前的模型欧拉角
Vector3 temp_modelEuler = model.transform.eulerAngles;
// 摄像机旋转...
// 摄像机旋转后将模型原来的欧拉角再赋给模型
model.transform.eulerAngles = temp_modelEuler;
}
当你这样做以后,会有一个惊喜发现,当你转动摄像机而不移动角色时,模型不会动,而当你移动时,模型会自动旋转到摄像机的朝向。原因是因为当你旋转摄像机时,PlayerController游戏对象也进行了旋转,这时整个玩家的前方已经变了。当移动时,会执行旋转代码,这时我们往前走,前方就是摄像机的朝向。所以模型会转到摄像机的朝向。
注意下面代码的dirVec为根据PlayerController游戏对象的方向得到的玩家移动方向,所以前方是PlayerController的前方。当旋转摄像机时,实际上是旋转PlayerController对象,然后摄像机跟着旋转。
// 计算玩家的方向 这个transform.forward是PlayerInput挂载的游戏对象,也就是PlayerController的transform.forward
// 所以dirVec得到的是以PlayerController的transform.forward为前方的方向
dirVec = m_dirAxis.x * transform.right + m_dirAxis.y * transform.forward;
// ...
// 只在有速度时能够旋转 防止原地旋转
if (pi.dirMag > 0.1f)
{
// 运用旋转 使用Slerp进行效果优化
model.transform.forward = Vector3.Slerp(model.transform.forward, pi.dirVec, 0.3f);
}
为了更好的视觉效果,我们常常希望摄像机能相对于角色的移动有一个延迟移动。要实现这种效果,首先我们在摄像机上级再添加一个父对象CameraPos,用来作为摄像机的位置。好了,现在Camera是不是PlayerController的游戏对象都可以了。但是CameraController脚本要挂载到CameraPos身上了。
现在,我们要在CameraController中获取到Camera游戏对象,再通过CameraPos的位置信息来控制Camera游戏对象的位置。
// 摄像机游戏对象的移动和旋转
camera.transform.eulerAngles = this.transform.eulerAngles;
// 摄像机的位置通过Lerp来实现一种延迟移动的效果
camera.transform.position = Vector3.SmoothDamp(
camera.transform.position, this.transform.position, ref temp_dampValue, 0.1f);
cameraHandle不使用Rotate而是通过eulerAngle来实现旋转是为了避免同位角产生的问题。
摄像机游戏对象用上SmoothDamp之后,旋转也会受到SmoothDamp的影响,而且模型会抖动(也可能是摄像机在抖动)。这是因为我们的CameraController脚本的逻辑是在Update中执行的,而我们的移动代码是在FixedUpdate中执行的,所以会导致一些不匹配。为了解决这个问题,我们要将CameraController中的Update改为FixedUpdate。同时deltaTime也要改为fixedDeltaTime。
// Update is called once per frame
void FixedUpdate()
{
// 得到摄像机旋转前的模型欧拉角
Vector3 temp_modelEuler = model.transform.eulerAngles;
// CameraPos旋转...
// 左右旋转时旋转PlayerHandle
playerHandle.transform.Rotate(Vector3.up, pi.cameraRight * horizontalSensitivity * Time.fixedDeltaTime);
// 上下旋转时旋转CameraHandle
temp_eulerX -= pi.cameraUp * verticalSensitivity * Time.fixedDeltaTime;
// 限制俯仰角
temp_eulerX = Mathf.Clamp(temp_eulerX, -40, 30);
// 赋值localEulerAngles
cameraHandle.transform.localEulerAngles = new Vector3(temp_eulerX , 0, 0);
// 摄像机的位置通过SmoothDamp来实现一种延迟移动的效果
camera.transform.position = Vector3.SmoothDamp(
camera.transform.position, this.transform.position, ref temp_dampValue, 0.1f);
//camera.transform.position = this.transform.position;
// 摄像机游戏对象的移动和旋转
camera.transform.eulerAngles = this.transform.eulerAngles;
// 摄像机旋转后将模型原来的欧拉角再赋给模型
model.transform.eulerAngles = temp_modelEuler;
}
不仅仅是移动时摄像机会有延迟,玩家旋转视角时也有。在这个已搭建的架构下我没有办法将摄像机的旋转视角和追踪玩家区分开,毕竟都是靠一段代码来实现的。我能做的就是,让玩家在没有移动时的SmoothDamp的time参数最小,移动起来的时候最大。方法如下。最关键的就是dampTime参数乘上移动输入模长,或者刚体速度模长。这样能做到仅仅移动时才会发生延迟,效果会好很多,至少延迟显得没那么烦人了。顺便提一下,由于我们的摄像机要在FixedUpdate里面移动,所以对于移动视角来说帧率只有50帧。
// 摄像机的位置通过SmoothDamp来实现一种延迟移动的效果
camera.transform.position = Vector3.SmoothDamp(
camera.transform.position,
this.transform.position,
ref temp_dampValue,
0.12f * pi.dirMag);
这是以往项目的三人称摄像机方案。摄像机是单独在外的,完全通过脚本使摄像机跟随玩家。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ThirdPersonCamera : MonoBehaviour
{
// 鼠标灵敏度
public float mouseSensitivity = 10f;
// 第三人称目标
public Transform target;
// 摄像机离目标的位置
public float distanceFromTarget = 2f;
// y方向的限制
public Vector2 pitchMinMax = new Vector2(-40f, 85f);
// 旋转运动平滑时间
public float rotationSmoothTime = 0.12f;
// 是否锁定鼠标
public bool lockCursor;
// x方向
private float yaw;
// y方向
private float pitch;
// 旋转运动平滑速度
Vector3 rotationSmoothVelocity;
// 当前的旋转
Vector3 currentRotation;
void Start()
{
if(lockCursor)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
// 使用LateUpdate在target.position设置好以后设置摄像机的位置
void LateUpdate()
{
yaw += Input.GetAxis("Mouse X") * mouseSensitivity;
pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity;
pitch = Mathf.Clamp(pitch, pitchMinMax.x, pitchMinMax.y);
currentRotation = Vector3.SmoothDamp(currentRotation, new Vector3(pitch, yaw, 0f), ref rotationSmoothVelocity, rotationSmoothTime);
Vector3 targetRotation = currentRotation;
transform.eulerAngles = targetRotation;
// 摄像机的位置设置在目标位置减去自身z轴方向上的特定距离
transform.position = target.position - transform.forward * distanceFromTarget;
}
}
似乎从老师的摄像机控制方法中找到了一种制作第三人称射击游戏的摄像机设计思路,如果角色的模型和PlayerHandle是一起旋转的,那么采用今天的方法做出来的摄像机就可以满足第三人称射击游戏的需求。以后做战争机器复刻应该用得上。