Unity基础包 刚体TPS类 3个第三人称脚本的研究
发表于2017-11-07
下面给大家介绍下刚体TPS类第三人称脚本的研究。第三人称的脚本有三个,一个是移动基础,一个是用户控制,还有一个会自动寻路的AI控制,关于NavMeshAgent会用是会用,但是还是比较好奇他的一个内部实现的。
移动基础的代码是有点多的,所以我们按照逻辑顺序线看用户控制的ThirdPersonUserControl脚本:
- // 用户控制器,基于ThirdPersonCharacter移动类这个脚本
- [RequireComponent(typeof (ThirdPersonCharacter))]
- public class ThirdPersonUserControl : MonoBehaviour
- {
- private ThirdPersonCharacter m_Character; //ThirdPersonCharacter脚本的引用
- private Transform m_Cam; //主相机的位置
- 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("警告:不存在一个主相机,球需要一个tag为MainCamera的相机,来做相机关联的移动");
- }
- // 获取第三人称的移动脚本,这个不能为空
- m_Character = GetComponent<ThirdPersonCharacter>();
- }
- // 更新
- private void Update()
- {
- if (!m_Jump) //不在跳跃状态下,如果读到跳跃则更新变量
- {
- m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");
- }
- }
- // 固定更新,用于物理的同步
- private void FixedUpdate()
- {
- // 获取用户的输入
- float h = CrossPlatformInputManager.GetAxis("Horizontal");
- float v = CrossPlatformInputManager.GetAxis("Vertical");
- bool crouch = Input.GetKey(KeyCode.C);
- // 计算移动方向,并传递给角色
- if (m_Cam != null)
- {
- // 计算相机关联方向,这边同样强调了前方向
- m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
- m_Move = v*m_CamForward h*m_Cam.right;
- }
- else
- {
- // 当没有相机时,直接以世界坐标轴作为参考
- m_Move = v*Vector3.forward h*Vector3.right;
- }
- #if !MOBILE_INPUT
- // 走路速度减半
- if (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
- #endif
- // 将所有的参数传递给移动类
- m_Character.Move(m_Move, crouch, m_Jump);
- m_Jump = false; //跳跃是个冲力,只要传一次就够了
- }
- }
这个脚本主要是读取到数据后调用到了ThirdPersonCharacter的脚本,这个脚本稍微有点复杂,但因为只是调用到了Move函数,所以按照Move函数其中代码的顺序看下去就行了:
- // 第三人称移动类,这边没有相机层,并且使用的是刚体和胶囊碰撞的组合,在用户控制的脚本ThirdPersonUserControl中只用到了Move方法来控制角色
- [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; //基础包中的特殊参数,在跟别的情况下最好移除该参数,腿偏移值
- [SerializeField] float m_MoveSpeedMultiplier = 1f; //移动速度的乘子
- [SerializeField] float m_AnimSpeedMultiplier = 1f; //动画播放速度的乘子
- [SerializeField] float m_GroundCheckDistance = 0.1f; //地面检查的距离
- Rigidbody m_Rigidbody; //刚体、动画、胶囊碰撞体
- Animator m_Animator;
- CapsuleCollider m_Capsule;
- 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; //胶囊的中心
- bool m_Crouching; //是否在蹲伏状态
- // 初始化动画、刚体和胶囊碰撞体
- void Start()
- {
- m_Animator = GetComponent<Animator>();
- m_Rigidbody = GetComponent<Rigidbody>();
- m_Capsule = GetComponent<CapsuleCollider>();
- m_CapsuleHeight = m_Capsule.height;
- m_CapsuleCenter = m_Capsule.center;
- m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ; //刚体冻结旋转,旋转由脚本来控制
- m_OrigGroundCheckDistance = m_GroundCheckDistance; //保存一下地面检查值
- }
- // 移动!这是在FixedUpdate中调用的
- public void Move(Vector3 move, bool crouch, bool jump)
- {
- // 将一个世界坐标的输入转换为本地相关的转向和前进速度,这需要考虑到角色头部的方向
- if (move.magnitude > 1f) move.Normalize(); //向量大于1,则变为单位向量
- move = transform.InverseTransformDirection(move); //转换为本地坐标
- CheckGroundStatus(); //判断当前地面的状态
- // 根据地面的法向量,产生一个对应平面的速度方向,第三次了吧
- move = Vector3.ProjectOnPlane(move, m_GroundNormal);
- 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)
- {
- if (m_Crouching) return;
- m_Capsule.height = m_Capsule.height / 2f;
- m_Capsule.center = m_Capsule.center / 2f;
- m_Crouching = true; //把正在蹲下设置为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, ~0, QueryTriggerInteraction.Ignore)) //这边的意思是从角色底部向上丢一个球,然后那个k_Half相关的参数是为了放置在丢的时候就碰到了地面,而做的向上偏移
- {
- 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, ~0, 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);
- if (!m_IsGrounded)
- {
- m_Animator.SetFloat("Jump", m_Rigidbody.velocity.y);
- }
- // 计算哪只脚是在后面的,所以可以判断跳跃动画中哪只脚先离开地面
- // 这里的代码依赖于特殊的跑步循环,假设某只脚会在未来的0到0.5秒内超越另一只脚
- float runCycle = Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime m_RunCycleLegOffset, 1); //获取当前是在哪个脚,Repeat相当于取模
- float jumpLeg = (runCycle < k_Half ? 1 : -1) * m_ForwardAmount;
- if (m_IsGrounded)
- {
- m_Animator.SetFloat("JumpLeg", jumpLeg);
- }
- // 这边的方法允许我们在inspector视图中调整动画的速率,他会因为根运动影响移动的速度
- 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);
- m_GroundCheckDistance = m_Rigidbody.velocity.y < 0 ? m_OrigGroundCheckDistance : 0.01f; //上升的时候不判断是否在地面上
- }
- // 地面的处理
- void HandleGroundedMovement(bool crouch, bool jump)
- {
- // 确定当前是否能跳
- if (jump && !crouch && m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Grounded"))
- {
- // 跳!
- m_Rigidbody.velocity = new Vector3(m_Rigidbody.velocity.x, m_JumpPower, m_Rigidbody.velocity.z); //保存x、z轴速度,并给以y轴向上的速度
- m_IsGrounded = false;
- m_Animator.applyRootMotion = false;
- m_GroundCheckDistance = 0.1f; //这个直接用0.01f也是没问题的吧
- }
- }
- // 应用附加转弯
- void ApplyExtraTurnRotation()
- {
- // 帮助角色快速转向,这是动画中根旋转的附加项
- float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount); //根据移动的速度计算出当前转向的速度
- transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0); //总之转!
- }
- // 这个方法没被用到
- public void OnAnimatorMove()
- {
- // 我们实现了使用这个方法来代替基础的移动,这个方法允许我们移除位置的速度
- if (m_IsGrounded && Time.deltaTime > 0)
- {
- Vector3 v = (m_Animator.deltaPosition * m_MoveSpeedMultiplier) / Time.deltaTime; //根据动画当前要移动位置,计算出方向
- // 保护一下y轴的移动速度
- v.y = m_Rigidbody.velocity.y;
- m_Rigidbody.velocity = v;
- }
- }
- // 判断当前地面的状态
- void CheckGroundStatus()
- {
- RaycastHit hitInfo;
- #if UNITY_EDITOR
- // 在场景中显示地面检查线,从脚上0.1米处往下射m_GroundCheckDistance的距离,预制体默认是0.3
- Debug.DrawLine(transform.position (Vector3.up * 0.1f), transform.position (Vector3.up * 0.1f) (Vector3.down * m_GroundCheckDistance));
- #endif
- // 0.1的射线是比较小的,基础包中预制体所设置的0.3是比较好的
- if (Physics.Raycast(transform.position (Vector3.up * 0.1f), Vector3.down, out hitInfo, m_GroundCheckDistance))
- {
- m_GroundNormal = hitInfo.normal; //射到了,保存法向量,改变变量,将动画的applyRootMotion置为true,true的含义是应用骨骼节点的位移,就是说动画的运动会对实际角色坐标产生影响,用于精确的播放动画
- m_IsGrounded = true;
- m_Animator.applyRootMotion = true;
- }
- else
- {
- m_IsGrounded = false;
- m_GroundNormal = Vector3.up;
- m_Animator.applyRootMotion = false; //不过我感觉这边不设为false也是可以的
- }
- }
- }
最后来看看AI,不过说实话,我不太了解NavMeshAgent究竟是怎样控制了Animator进行移动的,需要注意的是这个脚本同样是基于以上的TPSCharacter脚本来实现的。
下面的脚本是AICharacterControl:
- // 使用网格寻路,A*算法,这玩意还是挺方便的,就是隐藏了实现细节,可恶的unity
- [RequireComponent(typeof (NavMeshAgent))]
- [RequireComponent(typeof (ThirdPersonCharacter))]
- public class AICharacterControl : MonoBehaviour
- {
- public NavMeshAgent agent { get; private set; } // 寻路的网格
- public ThirdPersonCharacter character { get; private set; } // 角色的移动类
- public Transform target; // 目标
- private void Start()
- {
- // 获取组件,不应该为空所以不用检查了,如果为空反正会报错,我们这边不用判断,unity基础包就是这个意思
- agent = GetComponentInChildren<NavMeshAgent>();
- character = GetComponent<ThirdPersonCharacter>();
- agent.updateRotation = false; //不允许NavMesh来旋转角色
- agent.updatePosition = true;
- }
- private void Update()
- {
- if (target != null)
- agent.SetDestination(target.position); //设置目标
- //if (agent.remainingDistance > agent.stoppingDistance) //如果到达目标距离内则停下,这边是NavMash已经实现了吗
- // character.Move(agent.desiredVelocity, false, false);
- else
- character.Move(Vector3.zero, false, false); //没有目标的情况下不动
- }
- public void SetTarget(Transform target)
- {
- this.target = target;
- }
- }
非常简洁,自己写一个A*的话……反正我只弄了个方格下的A*寻路。