Parkour Climbing System开发日记
------类刺客信条跑酷系统
Day1 摄像头脚本
在unity中,xyz轴是右手坐标系,即x水平向右,y垂直向上,z水平向前
csharp
public class CameraController : MonoBehaviour
{
//摄像机跟随的目标
[SerializeField] Transform followTarget;
// Update is called once per frame
void Update()
{
//摄像机放在目标后面5个单位的位置
transform.position = followTarget.position - new Vector3(0, 0, 5);
}
}
怎么旋转这个相机呢?

摄像机向后移动的参量乘一个水平旋转角度
所以,引入四元数点欧拉Quaternion.Euler
这个水平视角旋转角度需要绕y轴的旋转角度,还需要鼠标控制这个角度
并且,当摄像头旋转的时候,摄像头始终对着player
csharp
public class CameraController : MonoBehaviour
{
//摄像机跟随的目标
[SerializeField] Transform followTarget;
//距离
[SerializeField] float distance;
//绕y轴的旋转角度
float rotationY;
private void Update()
{
//鼠标x轴控制rotationY
rotationY += Input.GetAxis("Mouse X");
//水平视角旋转参量
//想要水平旋转视角,所以需要的参量为绕y轴旋转角度
var horizontalRotation = Quaternion.Euler(0, rotationY, 0);
//摄像机放在目标后面5个单位的位置
transform.position = followTarget.position - horizontalRotation * new Vector3(0, 0, distance);
//摄像机始终朝向目标
transform.rotation = horizontalRotation;
}
}

完成了水平视角的旋转
让相机垂直旋转

还需要在垂直视角旋转的时候合理的限幅:让视角最高不超过45°,最低到人物的胸部位置
csharp
public class CameraController : MonoBehaviour
{
//摄像机跟随的目标
[SerializeField] Transform followTarget;
[SerializeField] float rotationSpeed = 1.5f;
//距离
[SerializeField] float distance;
//绕y轴的旋转角度------水平视角旋转
float rotationY;
//绕x轴的旋转角度------垂直视角旋转
float rotationX;
//限制rotationX幅度
[SerializeField] float minVerticalAngle = -20;
[SerializeField] float maxVerticalAngle = 45;
//框架偏移向量------摄像机位置视差偏移
[SerializeField] Vector2 frameOffset;
private void Update()
{
//鼠标x轴控制rotationY
rotationY += Input.GetAxis("Mouse X") * rotationSpeed;
//鼠标y轴控制rotationX
rotationX += Input.GetAxis("Mouse Y") * rotationSpeed;
//限制rotationX幅度
rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);
//视角旋转参量
//想要水平旋转视角,所以需要的参量为绕y轴旋转角度
var targetRotation = Quaternion.Euler(rotationX, rotationY, 0);
//摄像机的焦点位置
var focusPosition = followTarget.position + new Vector3(frameOffset.x, frameOffset.y, 0);
//摄像机放在目标后面5个单位的位置
transform.position = focusPosition - targetRotation * new Vector3(0, 0, distance);
//摄像机始终朝向目标
transform.rotation = targetRotation;
}
}

大致实现了摄像机跟随人物进行旋转
还需要一些细节调整:
csharp
private void Start()
{
//隐藏光标
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
考虑到存在大多数角色控制器都有控制反转的选项
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
//摄像机跟随的目标
[SerializeField] Transform followTarget;
[SerializeField] float rotationSpeed = 1.5f;
//距离
[SerializeField] float distance;
//绕y轴的旋转角度------水平视角旋转
float rotationY;
//绕x轴的旋转角度------垂直视角旋转
float rotationX;
//限制rotationX幅度
[SerializeField] float minVerticalAngle = -20;
[SerializeField] float maxVerticalAngle = 45;
//框架偏移向量------摄像机位置视差偏移
[SerializeField] Vector2 frameOffset;
//视角控制反转
[Header("视角控制反转:invertX是否反转垂直视角,invertY是否反转水平视角")]
[SerializeField] bool invertX;
[SerializeField] bool invertY;
float invertXValue;
float invertYValue;
private void Start()
{
//隐藏光标
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
//视角控制反转参数
invertXValue = (invertX)? -1 : 1;
invertYValue = (invertY)? -1 : 1;
//水平视角控制------鼠标x轴控制rotationY
rotationY += Input.GetAxis("Mouse X") * rotationSpeed * invertYValue;
//垂直视角控制------鼠标y轴控制rotationX
rotationX += Input.GetAxis("Mouse Y") * rotationSpeed * invertXValue;
//限制rotationX幅度
rotationX = Mathf.Clamp(rotationX, minVerticalAngle, maxVerticalAngle);
//视角旋转参量
//想要水平旋转视角,所以需要的参量为绕y轴旋转角度
var targetRotation = Quaternion.Euler(rotationX, rotationY, 0);
//摄像机的焦点位置
var focusPosition = followTarget.position + new Vector3(frameOffset.x, frameOffset.y, 0);
//摄像机放在目标后面5个单位的位置
transform.position = focusPosition - targetRotation * new Vector3(0, 0, distance);
//摄像机始终朝向目标
transform.rotation = targetRotation;
}
}
我通常喜欢这样选择,垂直反转(鼠标向上就看上面),水平不反转(鼠标向左就看左边)
就是让摄像机视角和鼠标移动方向对我来说是同步的,相当于第一人称视角控制的习惯
Day2 第三人称人物控制脚本
前序准备
先创建个人物模型( 从Mixamo下载的)
导入unity中,选择模型后点开inspector-Materials-Textures,选一个文件夹存放纹理


OK,下面就开始为这个角色写控制脚本吧!
最简化的第三人称角色控制
csharp
public class PlayerController : MonoBehaviour
{
[SerializeField]float moveSpeed = 5f;
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
//标准化 moveInput 向量
var moveInput = new Vector3(h, 0, v).normalized;
transform.position += moveInput * moveSpeed * Time.deltaTime;
}
}
需要注意的:
-
.normalized
:如果不进行标准化,
moveInput
向量的长度会变得大于1(具体来说,比如h,v长度都为1,\(\sqrt{h^2 + 0^2 + v^2} = \sqrt{1^2 + 0^2 + 1^2} = \sqrt{2} \approx 1.414\))。这意味着在对角线方向上移动时,玩家的移动速度会比只在一个方向上移动时快。为了确保玩家在所有方向上移动时速度一致,需要对向量进行标准化。 -
Time.deltaTime
:Time.deltaTime
是Unity引擎提供的一个浮点数,表示从上一帧到当前帧所用的时间(以秒为单位)。使用Time.deltaTime
可以确保玩家的移动速度在不同帧率下保持一致。如果不使用Time.deltaTime
,在高帧率下玩家会移动得更快,在低帧率下玩家会移动得更慢。
改进
上面这样显然不能满足角色控制,因为当我们按下前进方向键的时候,人物并没有根据当前摄像机显示的方向移动
还需要进行如下改进:
在CameraController.cs里面加入
csharp
//水平方向的旋转,返回摄像机的水平旋转四元数。
public Quaternion PlanarRotation => Quaternion.Euler(0, rotationY, 0);
这里提一句C#中的特性:
大多数语言中想要获取一个返回值,需要定义一个函数,然后返回
csharppublic Quaternion GetPlanarRotation() { return Quaternion.Euler(0, rotationY, 0); }
但是C#可以优雅的利用表达式主体定义的属性,直接获取这个属性
然后在PlayerController.cs里调用这个返回值
csharp
public class PlayerController : MonoBehaviour
{
[SerializeField]float moveSpeed = 5f;
CameraController cameraController;
private void Awake()
{
//相机控制器设置为main camera
cameraController = Camera.main.GetComponent<CameraController>();
}
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float moveAmount = Mathf.Abs(h) + Mathf.Abs(v);
//标准化 moveInput 向量
var moveInput = new Vector3(h, 0, v).normalized;
//让人物移动方向关联相机的朝向
var moveDir = cameraController.PlanarRotation * moveInput;
//每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
//没有输入就不更新转向,也就不会回到初始朝向
if (moveAmount > 0)
{
//帧同步移动
transform.position += moveDir * moveSpeed * Time.deltaTime;
//人物模型转起来:让人物朝向与移动方向一致
transform.rotation = Quaternion.LookRotation(moveDir);
}
}
}
这里解决了一个问题:
当方向键输入结束,人物模型朝向又回到了初始状态朝向
所以需要实时响应输入
- if (moveAmount > 0)只有输入的时候才会更新人物朝向
- 确保模型始终朝向移动方向。
但是还有一个问题:
人物朝向切换太快了,需要设置一个转向速度,让人物从当前朝向到目标朝向慢慢转向
csharp
[SerializeField]float rotationSpeed = 10f;
Quaternion targetRotation;
csharp
//每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
//没有输入就不更新转向,也就不会回到初始朝向
if (moveAmount > 0)
{
//帧同步移动
transform.position += moveDir * moveSpeed * Time.deltaTime;
//人物模型转起来:让人物朝向与移动方向一致
targetRotation = Quaternion.LookRotation(moveDir);
}
//更新transform.rotation:让人物从当前朝向到目标朝向慢慢转向
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
实现效果如下:

该部分完整代码:
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[Header("玩家属性")]
[SerializeField]float moveSpeed = 5f;
[SerializeField]float rotationSpeed = 10f;
Quaternion targetRotation;
CameraController cameraController;
private void Awake()
{
//相机控制器设置为main camera
cameraController = Camera.main.GetComponent<CameraController>();
}
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float moveAmount = Mathf.Abs(h) + Mathf.Abs(v);
//标准化 moveInput 向量
var moveInput = new Vector3(h, 0, v).normalized;
//让人物移动方向关联相机的朝向
var moveDir = cameraController.PlanarRotation * moveInput;
//每次判断moveAmount的时候,确保只有在玩家实际移动时才会更新移动+转向
//没有输入就不更新转向,也就不会回到初始朝向
if (moveAmount > 0)
{
//帧同步移动
transform.position += moveDir * moveSpeed * Time.deltaTime;
//人物模型转起来:让人物朝向与移动方向一致
targetRotation = Quaternion.LookRotation(moveDir);
}
//更新transform.rotation:让人物从当前朝向到目标朝向慢慢转向
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
}
}