Unity基础包 刚体TPS类 3个第三人称脚本的研究

发表于2017-11-07
评论0 1.4k浏览

下面给大家介绍下刚体TPS类第三人称脚本的研究。第三人称的脚本有三个,一个是移动基础,一个是用户控制,还有一个会自动寻路的AI控制,关于NavMeshAgent会用是会用,但是还是比较好奇他的一个内部实现的。

 

移动基础的代码是有点多的,所以我们按照逻辑顺序线看用户控制的ThirdPersonUserControl脚本:

  1. // 用户控制器,基于ThirdPersonCharacter移动类这个脚本  
  2. [RequireComponent(typeof (ThirdPersonCharacter))]  
  3. public class ThirdPersonUserControl : MonoBehaviour  
  4. {  
  5.     private ThirdPersonCharacter m_Character; //ThirdPersonCharacter脚本的引用  
  6.     private Transform m_Cam;                  //主相机的位置  
  7.     private Vector3 m_CamForward;             //当前相机的正前方  
  8.     private Vector3 m_Move;                   //根据相机的正前方和用户的输入,计算世界坐标相关的移动方向  
  9.     private bool m_Jump;                        
  10.   
  11.     // 初始化  
  12.     private void Start()  
  13.     {  
  14.         // 获取主相机,这边的实现跟球控制器是一样的  
  15.         if (Camera.main != null)  
  16.         {  
  17.             m_Cam = Camera.main.transform;  
  18.         }  
  19.         else  
  20.         {  
  21.             // 在这个例子中我们使用世界坐标来控制,也许这不是他们做希望的,不过我们至少警告一下他们  
  22.             Debug.LogWarning("警告:不存在一个主相机,球需要一个tag为MainCamera的相机,来做相机关联的移动");  
  23.         }  
  24.   
  25.         // 获取第三人称的移动脚本,这个不能为空  
  26.         m_Character = GetComponent<ThirdPersonCharacter>();  
  27.     }  
  28.   
  29.     // 更新  
  30.     private void Update()  
  31.     {  
  32.         if (!m_Jump)    //不在跳跃状态下,如果读到跳跃则更新变量  
  33.         {  
  34.             m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");  
  35.         }  
  36.     }  
  37.   
  38.   
  39.     // 固定更新,用于物理的同步  
  40.     private void FixedUpdate()  
  41.     {  
  42.         // 获取用户的输入  
  43.         float h = CrossPlatformInputManager.GetAxis("Horizontal");  
  44.         float v = CrossPlatformInputManager.GetAxis("Vertical");  
  45.         bool crouch = Input.GetKey(KeyCode.C);  
  46.   
  47.         // 计算移动方向,并传递给角色  
  48.         if (m_Cam != null)  
  49.         {  
  50.             // 计算相机关联方向,这边同样强调了前方向  
  51.             m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;  
  52.             m_Move = v*m_CamForward   h*m_Cam.right;  
  53.         }  
  54.         else  
  55.         {  
  56.             // 当没有相机时,直接以世界坐标轴作为参考  
  57.             m_Move = v*Vector3.forward   h*Vector3.right;  
  58.         }  
  59. #if !MOBILE_INPUT  
  60.         // 走路速度减半  
  61.         if (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;  
  62. #endif  
  63.   
  64.         // 将所有的参数传递给移动类  
  65.         m_Character.Move(m_Move, crouch, m_Jump);  
  66.         m_Jump = false//跳跃是个冲力,只要传一次就够了  
  67.     }  
  68. }  

这个脚本主要是读取到数据后调用到了ThirdPersonCharacter的脚本,这个脚本稍微有点复杂,但因为只是调用到了Move函数,所以按照Move函数其中代码的顺序看下去就行了:

  1. // 第三人称移动类,这边没有相机层,并且使用的是刚体和胶囊碰撞的组合,在用户控制的脚本ThirdPersonUserControl中只用到了Move方法来控制角色  
  2. [RequireComponent(typeof(Rigidbody))]  
  3. [RequireComponent(typeof(CapsuleCollider))]  
  4. [RequireComponent(typeof(Animator))]  
  5. public class ThirdPersonCharacter : MonoBehaviour  
  6. {  
  7.     [SerializeField] float m_MovingTurnSpeed = 360; //移动中转向的速度  
  8.     [SerializeField] float m_StationaryTurnSpeed = 180; //站立中转向的速度  
  9.     [SerializeField] float m_JumpPower = 12f;   //跳跃产生的力量  
  10.     [Range(1f, 4f)][SerializeField] float m_GravityMultiplier = 2f; //重力乘子  
  11.     [SerializeField] float m_RunCycleLegOffset = 0.2f;  //基础包中的特殊参数,在跟别的情况下最好移除该参数,腿偏移值  
  12.     [SerializeField] float m_MoveSpeedMultiplier = 1f;  //移动速度的乘子  
  13.     [SerializeField] float m_AnimSpeedMultiplier = 1f;  //动画播放速度的乘子  
  14.     [SerializeField] float m_GroundCheckDistance = 0.1f;    //地面检查的距离  
  15.   
  16.     Rigidbody m_Rigidbody;  //刚体、动画、胶囊碰撞体  
  17.     Animator m_Animator;  
  18.     CapsuleCollider m_Capsule;  
  19.   
  20.     bool m_IsGrounded;  //是否在地面上  
  21.     float m_OrigGroundCheckDistance;    //地面检查距离的起源值  
  22.     const float k_Half = 0.5f;  
  23.     float m_TurnAmount; //转向值  
  24.     float m_ForwardAmount;  //前进值  
  25.     Vector3 m_GroundNormal; //地面法向量  
  26.     float m_CapsuleHeight;  //胶囊高度  
  27.     Vector3 m_CapsuleCenter;    //胶囊的中心  
  28.     bool m_Crouching;   //是否在蹲伏状态  
  29.   
  30.     // 初始化动画、刚体和胶囊碰撞体  
  31.     void Start()  
  32.     {  
  33.         m_Animator = GetComponent<Animator>();  
  34.         m_Rigidbody = GetComponent<Rigidbody>();  
  35.         m_Capsule = GetComponent<CapsuleCollider>();  
  36.         m_CapsuleHeight = m_Capsule.height;  
  37.         m_CapsuleCenter = m_Capsule.center;  
  38.   
  39.         m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ;   //刚体冻结旋转,旋转由脚本来控制  
  40.         m_OrigGroundCheckDistance = m_GroundCheckDistance;  //保存一下地面检查值  
  41.     }  
  42.   
  43.     // 移动!这是在FixedUpdate中调用的  
  44.     public void Move(Vector3 move, bool crouch, bool jump)  
  45.     {  
  46.   
  47.         // 将一个世界坐标的输入转换为本地相关的转向和前进速度,这需要考虑到角色头部的方向  
  48.         if (move.magnitude > 1f) move.Normalize();  //向量大于1,则变为单位向量  
  49.         move = transform.InverseTransformDirection(move);   //转换为本地坐标  
  50.   
  51.         CheckGroundStatus();    //判断当前地面的状态  
  52.   
  53.         // 根据地面的法向量,产生一个对应平面的速度方向,第三次了吧  
  54.         move = Vector3.ProjectOnPlane(move, m_GroundNormal);  
  55.         m_TurnAmount = Mathf.Atan2(move.x, move.z); //产生一个方位角,即与z轴的夹角,用于人物转向  
  56.         m_ForwardAmount = move.z;   //人物前进的数值  
  57.   
  58.         ApplyExtraTurnRotation();   //应用附加转弯!  
  59.   
  60.         // 控制和速度处理,在地上和空中是不一样的  
  61.         if (m_IsGrounded)  
  62.         {  
  63.             HandleGroundedMovement(crouch, jump);   //地面处理  
  64.         }  
  65.         else  
  66.         {  
  67.             HandleAirborneMovement();   //空中处理  
  68.         }  
  69.   
  70.         ScaleCapsuleForCrouching(crouch);   //蹲下时缩小胶囊碰撞体  
  71.         PreventStandingInLowHeadroom(); //在没有足够的空间时,保持下蹲  
  72.   
  73.         // 将输入和其他状态传递给动画组件  
  74.         UpdateAnimator(move);  
  75.     }  
  76.   
  77.     // 缩小胶囊碰撞体  
  78.     void ScaleCapsuleForCrouching(bool crouch)  
  79.     {  
  80.         // 蹲下的一瞬间把胶囊高度和中心高度减半  
  81.         if (m_IsGrounded && crouch)  
  82.         {  
  83.             if (m_Crouching) return;  
  84.             m_Capsule.height = m_Capsule.height / 2f;  
  85.             m_Capsule.center = m_Capsule.center / 2f;  
  86.             m_Crouching = true//把正在蹲下设置为true,保证上面代码只执行一次  
  87.         }  
  88.         else  
  89.         {  
  90.             Ray crouchRay = new Ray(m_Rigidbody.position   Vector3.up * m_Capsule.radius * k_Half, Vector3.up); //创造一条刚体位置增加半径一半的位置,向上发射  
  91.             float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;    //射线长度,胶囊原高度减少半径一半的位置,  
  92.             if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, ~0, QueryTriggerInteraction.Ignore))  //这边的意思是从角色底部向上丢一个球,然后那个k_Half相关的参数是为了放置在丢的时候就碰到了地面,而做的向上偏移  
  93.             {  
  94.                 m_Crouching = true//碰到了,说明角色无法回到站立状态  
  95.                 return;  
  96.             }  
  97.             // 没有碰到,回到初始的状态  
  98.             m_Capsule.height = m_CapsuleHeight;  
  99.             m_Capsule.center = m_CapsuleCenter;  
  100.             m_Crouching = false;  
  101.         }  
  102.     }  
  103.   
  104.     // 在没有足够的空间时,保持下蹲  
  105.     void PreventStandingInLowHeadroom()  
  106.     {  
  107.         // 在只能下蹲的区域保持下蹲  
  108.         if (!m_Crouching)  
  109.         {  
  110.             Ray crouchRay = new Ray(m_Rigidbody.position   Vector3.up * m_Capsule.radius * k_Half, Vector3.up);  
  111.             float crouchRayLength = m_CapsuleHeight - m_Capsule.radius * k_Half;  
  112.             if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, ~0, QueryTriggerInteraction.Ignore))  
  113.             {  
  114.                 m_Crouching = true;  
  115.             }  
  116.         }  
  117.     }  
  118.   
  119.     // 更新动画组件  
  120.     void UpdateAnimator(Vector3 move)  
  121.     {  
  122.         // 更新动画的参数  
  123.         m_Animator.SetFloat("Forward", m_ForwardAmount, 0.1f, Time.deltaTime);  
  124.         m_Animator.SetFloat("Turn", m_TurnAmount, 0.1f, Time.deltaTime);  
  125.         m_Animator.SetBool("Crouch", m_Crouching);  
  126.         m_Animator.SetBool("OnGround", m_IsGrounded);  
  127.         if (!m_IsGrounded)  
  128.         {  
  129.             m_Animator.SetFloat("Jump", m_Rigidbody.velocity.y);  
  130.         }  
  131.   
  132.         // 计算哪只脚是在后面的,所以可以判断跳跃动画中哪只脚先离开地面  
  133.         // 这里的代码依赖于特殊的跑步循环,假设某只脚会在未来的0到0.5秒内超越另一只脚  
  134.         float runCycle = Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime   m_RunCycleLegOffset, 1);   //获取当前是在哪个脚,Repeat相当于取模  
  135.         float jumpLeg = (runCycle < k_Half ? 1 : -1) * m_ForwardAmount;  
  136.         if (m_IsGrounded)  
  137.         {  
  138.             m_Animator.SetFloat("JumpLeg", jumpLeg);  
  139.         }  
  140.   
  141.         // 这边的方法允许我们在inspector视图中调整动画的速率,他会因为根运动影响移动的速度  
  142.         if (m_IsGrounded && move.magnitude > 0)  
  143.         {  
  144.             m_Animator.speed = m_AnimSpeedMultiplier;  
  145.         }  
  146.         else  
  147.         {  
  148.             // 在空中的时候不用  
  149.             m_Animator.speed = 1;  
  150.         }  
  151.     }  
  152.   
  153.     // 空中的处理,注意空中跳跃和下蹲是不起作用的  
  154.     void HandleAirborneMovement()  
  155.     {  
  156.         // 根据乘子应用额外的重力  
  157.         Vector3 extraGravityForce = (Physics.gravity * m_GravityMultiplier) - Physics.gravity;  
  158.         m_Rigidbody.AddForce(extraGravityForce);  
  159.   
  160.         m_GroundCheckDistance = m_Rigidbody.velocity.y < 0 ? m_OrigGroundCheckDistance : 0.01f; //上升的时候不判断是否在地面上  
  161.     }  
  162.   
  163.     // 地面的处理  
  164.     void HandleGroundedMovement(bool crouch, bool jump)  
  165.     {  
  166.         // 确定当前是否能跳  
  167.         if (jump && !crouch && m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Grounded"))  
  168.         {  
  169.             // 跳!  
  170.             m_Rigidbody.velocity = new Vector3(m_Rigidbody.velocity.x, m_JumpPower, m_Rigidbody.velocity.z);    //保存x、z轴速度,并给以y轴向上的速度  
  171.             m_IsGrounded = false;  
  172.             m_Animator.applyRootMotion = false;  
  173.             m_GroundCheckDistance = 0.1f;   //这个直接用0.01f也是没问题的吧  
  174.         }  
  175.     }  
  176.   
  177.     // 应用附加转弯  
  178.     void ApplyExtraTurnRotation()  
  179.     {  
  180.         // 帮助角色快速转向,这是动画中根旋转的附加项  
  181.         float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount);    //根据移动的速度计算出当前转向的速度  
  182.         transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0);  //总之转!  
  183.     }  
  184.   
  185.     // 这个方法没被用到  
  186.     public void OnAnimatorMove()  
  187.     {  
  188.         // 我们实现了使用这个方法来代替基础的移动,这个方法允许我们移除位置的速度  
  189.         if (m_IsGrounded && Time.deltaTime > 0)  
  190.         {  
  191.             Vector3 v = (m_Animator.deltaPosition * m_MoveSpeedMultiplier) / Time.deltaTime;    //根据动画当前要移动位置,计算出方向   
  192.   
  193.             // 保护一下y轴的移动速度  
  194.             v.y = m_Rigidbody.velocity.y;  
  195.             m_Rigidbody.velocity = v;  
  196.         }  
  197.     }  
  198.   
  199.     // 判断当前地面的状态  
  200.     void CheckGroundStatus()  
  201.     {  
  202.         RaycastHit hitInfo;  
  203. #if UNITY_EDITOR  
  204.         // 在场景中显示地面检查线,从脚上0.1米处往下射m_GroundCheckDistance的距离,预制体默认是0.3  
  205.         Debug.DrawLine(transform.position   (Vector3.up * 0.1f), transform.position   (Vector3.up * 0.1f)   (Vector3.down * m_GroundCheckDistance));  
  206. #endif  
  207.         // 0.1的射线是比较小的,基础包中预制体所设置的0.3是比较好的  
  208.         if (Physics.Raycast(transform.position   (Vector3.up * 0.1f), Vector3.down, out hitInfo, m_GroundCheckDistance))  
  209.         {  
  210.             m_GroundNormal = hitInfo.normal;    //射到了,保存法向量,改变变量,将动画的applyRootMotion置为true,true的含义是应用骨骼节点的位移,就是说动画的运动会对实际角色坐标产生影响,用于精确的播放动画  
  211.             m_IsGrounded = true;  
  212.             m_Animator.applyRootMotion = true;  
  213.         }  
  214.         else  
  215.         {  
  216.             m_IsGrounded = false;  
  217.             m_GroundNormal = Vector3.up;  
  218.             m_Animator.applyRootMotion = false//不过我感觉这边不设为false也是可以的  
  219.         }  
  220.     }  
  221. }  

最后来看看AI,不过说实话,我不太了解NavMeshAgent究竟是怎样控制了Animator进行移动的,需要注意的是这个脚本同样是基于以上的TPSCharacter脚本来实现的。


下面的脚本是AICharacterControl:

  1. // 使用网格寻路,A*算法,这玩意还是挺方便的,就是隐藏了实现细节,可恶的unity  
  2. [RequireComponent(typeof (NavMeshAgent))]  
  3. [RequireComponent(typeof (ThirdPersonCharacter))]  
  4. public class AICharacterControl : MonoBehaviour  
  5. {  
  6.     public NavMeshAgent agent { getprivate set; }             // 寻路的网格  
  7.     public ThirdPersonCharacter character { getprivate set; } // 角色的移动类  
  8.     public Transform target;                                    // 目标  
  9.   
  10.   
  11.     private void Start()  
  12.     {  
  13.         // 获取组件,不应该为空所以不用检查了,如果为空反正会报错,我们这边不用判断,unity基础包就是这个意思  
  14.         agent = GetComponentInChildren<NavMeshAgent>();  
  15.         character = GetComponent<ThirdPersonCharacter>();  
  16.   
  17.         agent.updateRotation = false;   //不允许NavMesh来旋转角色  
  18.         agent.updatePosition = true;  
  19.     }  
  20.   
  21.   
  22.     private void Update()  
  23.     {  
  24.         if (target != null)  
  25.             agent.SetDestination(target.position);  //设置目标  
  26.   
  27.         //if (agent.remainingDistance > agent.stoppingDistance) //如果到达目标距离内则停下,这边是NavMash已经实现了吗  
  28.         //    character.Move(agent.desiredVelocity, false, false);  
  29.         else  
  30.             character.Move(Vector3.zero, falsefalse); //没有目标的情况下不动  
  31.     }  
  32.   
  33.   
  34.     public void SetTarget(Transform target)  
  35.     {  
  36.         this.target = target;  
  37.     }  
  38. }  

非常简洁,自己写一个A*的话……反正我只弄了个方格下的A*寻路。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引