【声明】此第三人称控制器是复现unity明星资产中第三人称控制器,如果有需要可以直接下载该资产并学习。我个人是不喜欢重复造轮子的行为的,但是官方的控制器我使用时有些不尽如人意的BUG。尽管如此,该控制器的效果让我觉得很不错,虽然无法实现无缝动画,不过个人项目应该先想办法让其动起来才行。
【版本】此项目基于Unity【2021.3lts】版本制作
本章是为了做到能够达到原型的程度,所以添加这些功能。大家在制作开发阶段可以根据自己的节奏来添加这些功能。总之一句话,需要的时候再去做。
重力制作
在我的代码中,角色的位移下方是由有一个TODO的,这是我们有打算制作的功能。
在垂直方向上,我们需要添加重力。
添加地面检测
我们不在地面上,才会受到重力的影响。所以我们去做一个地面检测,当不在地面上时,我们采取考虑竖直方向上的分量。
地面检测有很多种,射线法,碰撞体。我们为了学习,与官方制作的一致,使用物理的CheckSphere(请在unity中文手册中查看)
首先确定脚和地面的交界线,这个位置就是CC的底部。当然,我们如果制作下落动画,这个交界线就不能够这么严格。原因在于我们下楼梯时高度不足以触发下落。我们在这里为以后的这个需求提前做功课:
[Header("重力及下落")]
[Tooltip("地面宽容度")]
public float GroundedOffset = -0.14f;
我们将获取应用了宽容度的检测点,然后根据这个检测点判断碰撞
private void GroundedCheack()
{
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
}
重力
基本思想就是重力加速度
private void DoGravity()
{
if(Grounded)
{
//重置坠落计时器
_fallTimeOutDelta = FallTimeout;
//落地后我们停止垂直速度累加
if(_verticalVelocity<0.0f)
{
_verticalVelocity = -2f;
}
}
else
{
//此if用于决定是否是下落动画
if(_fallTimeOutDelta>=0.0f)
{
_fallTimeOutDelta -= Time.deltaTime;
}
else
{
//下落动画
}
}
if(_verticalVelocity<_terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}
_characterController.Move(targetDir.normalized * (_currentSpeed * Time.deltaTime)
+ new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
结尾
这就是重力的部分。那么做到这里就已经是比较不错的第三人称控制器了。我不打算增加跳跃功能,毕竟第三人称射击游戏可以不用管这个吧,可有可无的功能。等到真的有需要我再加。
代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class ThirdPlayerMoveController : MonoBehaviour
{
CharacterController _characterController;
PlayerInput _playerInput;
PlayerInputsMassage _inputsMassage;
GameObject _mainCamera;
Animator _anim;
public PlayerActionState playerActionState = PlayerActionState.Idle;
[Header("相机设置")]
public GameObject _cinemachineFollowTarget;
float _cinemachineTagertX;
float _cinemachineTagertY;
[Tooltip("相机仰角")]
public float TopClamp = 70.0f;
[Tooltip("相机俯角")]
public float BottomClamp = -30.0f;
[Tooltip("额外的度数覆盖摄像头。有用的微调相机位置时,锁定")]
public float CameraAngleOverride = 0.0f;
[Header("玩家设置")]
[Tooltip("这将决定普通行走时的速度")]
public float walkSpeed = 1.5f;
[Tooltip("这将决定跑步时的速度")]
public float _runSpeed = 5.0f;
private bool _isCrouch = false;
[Tooltip("这将决定蹲下行走的速度")]
public float _crouchSpeed = 1.0f;
[Tooltip("这将决定不同状态速度间切换的速度")]
public float SpeedChangeRate = 10.0f;
[Header("重力及下落")]
[Tooltip("重力加速度")]
public float Gravity = -15.0f;
[Tooltip("是否着地")]
public bool Grounded = true;
[Tooltip("检测半径")]
public float GroundedRadius = 0.28f;
[Tooltip("检测的层级")]
public LayerMask GroundLayers;
[Tooltip("地面宽容度")]
public float GroundedOffset = -0.14f;
[Tooltip("进入坠落所需时间")]
public float FallTimeout = 0.15f;
private float _fallTimeOutDelta;
private float _verticalVelocity;
private float _terminalVelocity = 53.0f;
private float _currentSpeed;
private float _targetRotation = 0.0f;
[Tooltip("角色光滑旋转时间")]
private float RotationSmoothTime = 0.12f;
[Tooltip("在角色光滑旋转过程中的速度")]
private float _rotationVelocity;
private float _threshold = 0.01f;
private bool IsCurrentDeviceMouse
{
get { return _playerInput.currentControlScheme == "KeyboardMouse"; }
}
private void Awake()
{
// get a reference to our main camera
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
}
}
// Start is called before the first frame update
void Start()
{
_characterController = GetComponent<CharacterController>();
_inputsMassage = GetComponent<PlayerInputsMassage>();
_playerInput = GetComponent<PlayerInput>();
_anim = GetComponentInChildren<Animator>();
}
private void Update()
{
DoGravity();
GroundedCheack();
Move();
}
private void LateUpdate()
{
CameraRotation();
}
/// <summary>
/// 相机追踪点的控制
/// </summary>
private void CameraRotation()
{
if(_inputsMassage.look.sqrMagnitude>_threshold)//look值大于误差代表有输入
{
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1f : Time.deltaTime;
_cinemachineTagertX += _inputsMassage.look.x * deltaTimeMultiplier;
_cinemachineTagertY += _inputsMassage.look.y * deltaTimeMultiplier;
}
_cinemachineTagertX = ClampAngle(_cinemachineTagertX, float.MinValue, float.MaxValue);
_cinemachineTagertY = ClampAngle(_cinemachineTagertY, BottomClamp, TopClamp);
_cinemachineFollowTarget.transform.rotation = Quaternion.Euler((-_cinemachineTagertY - CameraAngleOverride) * Settings.mouseYmoveTimes,
_cinemachineTagertX * Settings.mouseXmoveTimes, 0.0f);
}
private void Move()
{
_isCrouch = _inputsMassage.crouch;
//在这里进行状态的判断
PlayerStateJudge();
//首先将移动速度赋予临时变量,考虑到有可能在其他地方使用,我们将其存储起来
//_currentSpeed = walkSpeed;(转换为更加完善的速度控制)
float targetSpeed = playerActionState switch
{
PlayerActionState.Idle => 0f,
PlayerActionState.Walk => walkSpeed,
PlayerActionState.Run => _runSpeed,
_ => 0f
};
if (_isCrouch) targetSpeed = _crouchSpeed;
//玩家当前水平速度的参考
float currentHorizontalSpeed = new Vector3(_characterController.velocity.x, 0.0f, _characterController.velocity.z).magnitude;
//偏离度,保证目标速度与目前速度相差大才可以插值,避免小幅度的抽搐
float speedOffset = 0.1f;
//判断偏离度
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
currentHorizontalSpeed > targetSpeed + speedOffset)
{
_currentSpeed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed ,Time.deltaTime * SpeedChangeRate);
//四舍五入到小数点后3位
_currentSpeed = Mathf.Round(_currentSpeed * 1000f) / 1000f;
}
else
{
_currentSpeed = targetSpeed;
}
//判断是否进行移动输入
if (_inputsMassage.move == Vector2.zero) _currentSpeed = 0;
var currentInput = new Vector3(_inputsMassage.move.x, 0, _inputsMassage.move.y).normalized;
//单位向量的方向,或者说位移方向
if (_inputsMassage.move!=Vector2.zero)
{
_targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;
#region 在位移过程中的转向
float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
RotationSmoothTime);
transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
#endregion
}
Vector3 targetDir = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
_characterController.Move(targetDir.normalized * (_currentSpeed * Time.deltaTime)
+ new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
//TODO:这里的Move可以执行垂直方向的速度,直接加上垂直的Vector就可以
_anim.SetFloat("Speed", _currentSpeed);
_anim.SetBool("Crouch", _isCrouch);
}
private void DoGravity()
{
if(Grounded)
{
//重置坠落计时器
_fallTimeOutDelta = FallTimeout;
//落地后我们停止垂直速度累加
if(_verticalVelocity<0.0f)
{
_verticalVelocity = -2f;
}
}
else
{
//此if用于决定是否是下落动画
if(_fallTimeOutDelta>=0.0f)
{
_fallTimeOutDelta -= Time.deltaTime;
}
else
{
//下落动画
}
}
if(_verticalVelocity<_terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}
/// <summary>
/// 限制角度
/// </summary>
/// <param name="lfAngle"></param>
/// <param name="lfMin"></param>
/// <param name="lfMax"></param>
/// <returns></returns>
private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
/// <summary>
/// 对玩家状态进行判断
/// </summary>
private void PlayerStateJudge()
{
playerActionState = PlayerActionState.Idle;
if (_inputsMassage.move!=Vector2.zero)
{
playerActionState = PlayerActionState.Walk;
if (_inputsMassage.run)
playerActionState = PlayerActionState.Run;
}
}
private void GroundedCheack()
{
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
}
}