// ThirdPersonCharacter.cs
using UnityEngine;
namespace UnityStandardAssets.Characters.ThirdPerson
{
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Animator))]
public class ThirdPersonCharacter : MonoBehaviour
{
[SerializeField] float m_MovingTurnSpeed = 360;
[SerializeField] float m_StationaryTurnSpeed = 180;
[SerializeField] float m_JumpPower = 12f;
[Range(1f, 4f)] [SerializeField] float m_GravityMultiplier = 2f;
[SerializeField] float m_RunCycleLegOffset = 0.2f; //specific to the character in sample assets, will need to be modified to work with others
[SerializeField] float m_MoveSpeedMultiplier = 1f;
[SerializeField] float m_AnimSpeedMultiplier = 1f;
[SerializeField] float m_GroundCheckDistance = 0.1f;
Rigidbody m_Rigidbody;
Animator m_Animator;
bool m_IsGrounded;
float m_OrigGroundCheckDistance;
const float k_Half = 0.5f;
float m_TurnAmount;
float m_ForwardAmount;
Vector3 m_GroundNormal;
float m_CapsuleHeight;
Vector3 m_CapsuleCenter;
CapsuleCollider m_Capsule;
bool m_Crouching;
void Start()
{
m_Animator = GetComponent();
m_Rigidbody = GetComponent();
m_Capsule = GetComponent();
m_CapsuleHeight = m_Capsule.height;
m_CapsuleCenter = m_Capsule.center;
//冻结三个旋转轴 防止其运动时莫名其妙的"摔倒"
m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ;
//检查与地面的距离
m_OrigGroundCheckDistance = m_GroundCheckDistance;
}
///
/// 供调用的角色运动主方法
///
///
///
///
public void Move(Vector3 move, bool crouch, bool jump)
{
// 约束速度,在当前偏移控制下,当角色沿上下左右单个方向移动时偏移量的最大长度也不会超过1,当相邻两个方向叠加时限制最大速度不超过1
if (move.magnitude > 1f)
move.Normalize();
// 转换世界坐标到自身坐标 加上之后上下左右控制的是运动的方向 注释掉除前进之外其他控制的都是人物朝向
// 一开始没搞明白为啥会这样,后来明白了:如果参考坐标系是世界坐标系(摄像机坐标系),在计算角色朝向时,世界坐标的x轴和z轴永远不会重合
// 所以他们之间会一直有夹角,也就会一直旋转,而且角色旋转速度和夹角大小有关,所以向后的旋转是左右旋转的两倍
move = transform.InverseTransformDirection(move);
// 检测与地面距离(这里有个小bug就是当角色站在一个上升物体上时,角色的状态不是站住不动,而是有个跳跃的感觉)
CheckGroundStatus();
// 字面意思是计算与标准平面之间的法线 没发现注释前后有什么区别
move = Vector3.ProjectOnPlane(move, m_GroundNormal);
// 旋转的角度总是以z轴正方向为基准,这个函数求的是与z轴正方向的角偏移,当角色开始走直线时,他总是沿着自身z轴运动,所以夹角为0
m_TurnAmount = Mathf.Atan2(move.x, move.z);
// z轴方向的运动数值
m_ForwardAmount = move.z;
// 处理角色在静止和运动时的旋转速度
ApplyExtraTurnRotation();
// 处理在地运动逻辑
if (m_IsGrounded)
{
// 处理角色着地时运动状态
HandleGroundedMovement(crouch, jump);
}
// 处理滞空运动逻辑
else
{
// 处理角色下落速度和射线检测赋值
HandleAirborneMovement();
}
// 见名知意,处理下蹲时角色胶囊网格的缩放
ScaleCapsuleForCrouching(crouch);
// 障碍物主动使角色下蹲
PreventStandingInLowHeadroom();
// 更新状态机的状态
UpdateAnimator(move);
}
///
/// 处理下蹲状态的胶囊体缩放
///
///
void ScaleCapsuleForCrouching(bool crouch)
{
// 如果着地并且按下下蹲键(起跳下落的时候也是有个短暂的下蹲状态的)
if (m_IsGrounded && crouch)
{
// 如果正在下蹲(这个和crouch时不一样的,一个是标志一个是状态,一个是瞬时一个是连续)
if (m_Crouching)
return;
// 按下下蹲键的一瞬间胶囊体的高度和中心位置发生改变,而这之后m_Crouching才被置为true
m_Capsule.height = m_Capsule.height / 2f;
m_Capsule.center = m_Capsule.center / 2f;
m_Crouching = true;
}
// 否则
else
{
// 下蹲后头顶有障碍物时不能站起(注释掉的话角色会有位移的现象)
Ray crouchRay = new Ray(m_Rigidbody.position + Vector3.up * m_Capsule.radius * k_Half, Vector3.up);
// 向上检测障碍物的射线长度基于去掉半径的原胶囊体高度,因为射线测量的是不能站起来的高度
float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;
// 这里不是很明白为什么要用球形投射
if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, Physics.AllLayers, QueryTriggerInteraction.Ignore))
{
m_Crouching = true;
return;
}
// 还原为站立时的状态
m_Capsule.height = m_CapsuleHeight;
m_Capsule.center = m_CapsuleCenter;
m_Crouching = false;
}
}
///
/// 一开始没理解 其实这里要表达的意思是当有障碍物主动的接近角色上方时 角色会下蹲躲避
///
void PreventStandingInLowHeadroom()
{
if (!m_Crouching)
{
Ray crouchRay = new Ray(m_Rigidbody.position + Vector3.up * m_Capsule.radius * k_Half, Vector3.up);
float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;
if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, Physics.AllLayers, QueryTriggerInteraction.Ignore))
{
m_Crouching = true;
}
}
}
///
/// 更新动画状态机
///
///
void UpdateAnimator(Vector3 move)
{
// 更新动画状态机的参数
// 参数名 参数值 到达指定参数值的时间(会表现出一个加速的过程) 物体变化与时间有关与帧速率无关
m_Animator.SetFloat("Forward", m_ForwardAmount, 0.1f, Time.deltaTime);
m_Animator.SetFloat("Turn", m_TurnAmount, 0.1f, Time.deltaTime);
m_Animator.SetBool("Crouch", m_Crouching);
m_Animator.SetBool("OnGround", m_IsGrounded);
// 如果角色没有着地 将它竖直方向的刚体速度赋值给Jump
if (!m_IsGrounded)
{
m_Animator.SetFloat("Jump", m_Rigidbody.velocity.y);
}
// 根据角色运动时哪条腿在前决定起跳时的动作 让角色运动看起来没那么生硬 属于细节问题
// 如果删掉这些 你会发现角色每次起跳都是标准的"蛙跳" 看起来很搞笑
float runCycle =
Mathf.Repeat(
m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime + m_RunCycleLegOffset, 1);
float jumpLeg = (runCycle < k_Half ? 1 : -1) * m_ForwardAmount;
if (m_IsGrounded)
{
m_Animator.SetFloat("JumpLeg", jumpLeg);
}
// 只有当角色处于落地状态并且是在运动中时 增大或缩小角色的动画播放速率
if (m_IsGrounded && move.magnitude > 0)
{
m_Animator.speed = m_AnimSpeedMultiplier;
}
else
{
m_Animator.speed = 1;
}
}
///
/// 处理角色下落速度和射线检测赋值
///
void HandleAirborneMovement()
{
// 净增的重力加速向量
Vector3 extraGravityForce = (Physics.gravity * m_GravityMultiplier) - Physics.gravity;
// 用净增的加速度作为力加到角色上
m_Rigidbody.AddForce(extraGravityForce);
// 判断竖直方向角色速度是否小于0,是的话表示角色正在下落,否则落地检测射线距离等于最小阈值0.01f
m_GroundCheckDistance = m_Rigidbody.velocity.y < 0 ? m_OrigGroundCheckDistance : 0.01f;
}
///
/// 处理角色着地时运动状态
///
///
///
void HandleGroundedMovement(bool crouch, bool jump)
{
// 起跳的三个条件:1.按下了space键(只有按下的一瞬间为true) 2.没有下蹲 3.当前动画的第0层的名字是Grounded
if (jump && !crouch && m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Grounded"))
{
// 起跳后的运动状态,这里有学问了! 起跳后要保持起跳之前的横向运动状态,继承x轴和z轴的速度 起跳用的是速度
m_Rigidbody.velocity = new Vector3(m_Rigidbody.velocity.x, m_JumpPower, m_Rigidbody.velocity.z);
// 是否落地置否
m_IsGrounded = false;
// 动画不应用根节点(人物的动作不会影响到位置和旋转的变化)
m_Animator.applyRootMotion = false;
m_GroundCheckDistance = 0.1f;
}
}
///
/// 赋予角色在不同运动状态下的不同转向速度
///
void ApplyExtraTurnRotation()
{
// 使用插值控制旋转速度 (this is in addition to root rotation in the animation)
float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount);
transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0);
}
public void OnAnimatorMove()
{
// we implement this function to override the default root motion.
// this allows us to modify the positional speed before it's applied.
if (m_IsGrounded && Time.deltaTime > 0)
{
Vector3 v = (m_Animator.deltaPosition * m_MoveSpeedMultiplier) / Time.deltaTime;
// we preserve the existing y part of the current velocity.
v.y = m_Rigidbody.velocity.y;
m_Rigidbody.velocity = v;
}
}
///
/// 检测与地面距离
///
void CheckGroundStatus()
{
RaycastHit hitInfo;
#if UNITY_EDITOR
// helper to visualise the ground check ray in the scene view
Debug.DrawLine(transform.position + (Vector3.up * 0.1f), transform.position + (Vector3.up * 0.1f) + (Vector3.down * m_GroundCheckDistance));
#endif
// 如果角色自上向下的射线在地面上有接触,判断角色落地
if (Physics.Raycast(transform.position + (Vector3.up * 0.1f), Vector3.down, out hitInfo, m_GroundCheckDistance))
{
m_IsGrounded = true;
//m_GroundNormal = hitInfo.normal;
//m_Animator.applyRootMotion = true;
}
//否则为悬空
else
{
//开启下落姿势,下落速度增加,有缓冲效果,落地之前不能起跳
m_IsGrounded = false;
//TODO 这个暂时没看出有啥用
//m_GroundNormal = Vector3.up;
//m_Animator.applyRootMotion = false;
}
}
}
}
// ThirdPersonUserControl.cs
using System;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
namespace UnityStandardAssets.Characters.ThirdPerson
{
[RequireComponent(typeof(ThirdPersonCharacter))]
public class ThirdPersonUserControl : MonoBehaviour
{
private ThirdPersonCharacter m_Character; // 挂在角色上的ThirdPersonCharacter脚本
private Transform m_Cam; // 主摄像机的Transform组件
private Vector3 m_CamForward; // 主摄像机的朝向
private Vector3 m_Move; // 角色的移动控制
private bool m_Jump; // 是否起跳
///
/// 找到主摄像机和角色控制脚本
///
private void Start()
{
// 判断主摄像机是否存在
if (Camera.main != null)
{
m_Cam = Camera.main.transform;
}
else
{
Debug.LogWarning(
"Warning: no main camera found. Third person character needs a Camera tagged \"MainCamera\", for camera-relative controls.", gameObject);
// we use self-relative controls in this case, which probably isn't what the user wants, but hey, we warned them!
}
// 获取角色脚本
m_Character = GetComponent();
}
///
/// 只处理是否起跳
///
private void Update()
{
if (!m_Jump)
{
// 是否起跳和是否悬空不同 这里的是否起跳是个瞬时状态 只有当起跳键被按下的瞬间m_Jump返回true
m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");
}
}
///
/// 物理运动相关的要放在FixedUpdate里
///
private void FixedUpdate()
{
// 获取水平方向和竖直方向输入
float h = CrossPlatformInputManager.GetAxis("Horizontal");
float v = CrossPlatformInputManager.GetAxis("Vertical");
// C键下蹲 只要C键一直按下 crouch就一直返回true
bool crouch = Input.GetKey(KeyCode.C);
// 计算运动方向并传递给角色
if (m_Cam != null)
{
// 人物向前运动的方向取决于摄像机沿Z轴的正方向 此处使用等比缩放的作用就是屏蔽掉摄像机Y轴对运动的影响
// 如果不加后面的规范化 其实Y轴也已经屏蔽了 只不过数值很小 看起来好像Y轴依旧生效一样
m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
// 角色运动向量 = 摄像机Z轴方向 * Z轴方向偏移量 + 摄像机X轴方向 * X轴方向偏移量,因为是相加关系,当v和h都不为0时偏移量肯定要更大一些
m_Move = v * m_CamForward + h * m_Cam.right;
}
else
{
// 如果主摄像机不存在,角色运动方向没有了参考,只能按照世界坐标的正侧方向来运动
m_Move = v * Vector3.forward + h * Vector3.right;
}
#if !MOBILE_INPUT
// 如果移动端输入未指定,那么按下左Shift的话让角色移动速度变为一半
if (Input.GetKey(KeyCode.LeftShift))
m_Move *= 0.5f;
#endif
// 调用ThirdPersonCharacter的Move方法,实现移动(位移向量,是否下蹲,是否起跳)
m_Character.Move(m_Move, crouch, m_Jump);
// 无论是否起跳m_Jump在运动过程处理结束后都要置一次false
m_Jump = false;
}
}
}