1. 程式人生 > >Unity官方案例研究(第三人稱控制器)

Unity官方案例研究(第三人稱控制器)

因為在網上不容易找到解析官方案例的文章,最近也在研究第三人稱控制器,所以把我所知道的東西給大家分享一下.


主要的程式碼就這兩個指令碼,我已經大量註釋過了,現在給大家貼出來.

ThirdPersonUserControl指令碼

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; // 主攝像機的位置
        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("不存在主攝像機,需要有一個MinCamera來跟隨Player", gameObject);
            }

            // 獲取第三人稱的移動指令碼,這個不能為空  
            m_Character = GetComponent<ThirdPersonCharacter>();
        }


        private void Update()
        {
            if (!m_Jump)//不在跳躍狀態下,如果讀到跳躍則更新變數  
            {
                m_Jump = Input.GetButtonDown("Jump");
            }
        }


        // 固定幀數,用於物理的同步  
        private void FixedUpdate()
        {
            // 獲取使用者的輸入  
            //CrossPlatformInputManager是用來跨平臺使用的,input在windows平臺沒問題,在其他品臺可能就會出問題
            float h = CrossPlatformInputManager.GetAxis("Horizontal");
            float v = CrossPlatformInputManager.GetAxis("Vertical");
            bool crouch = Input.GetKey(KeyCode.C);

            // 計算移動方向,並傳遞給角色  
            if (m_Cam != null)
            {
                // 計算相機關聯方向,這邊同樣強調了前方向  
                //吧攝像機.forword的Y軸設為0並規範化
                m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
              //  print("m_Cam.forward  =  :" + m_Cam.forward);
              //  print("m_CamForward = "+m_CamForward);
              //  print("H = "+h);
              //  print("V = " + v);
                //v*攝像機的forWord
                //h*攝像機的right
                m_Move = v*m_CamForward + h*m_Cam.right;
              //  print("m_move = " + m_Move);
            }
            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指令碼

using UnityEngine;

namespace UnityStandardAssets.Characters.ThirdPerson
{
    // 第三人稱移動類,這邊沒有相機層,並且使用的是剛體和膠囊碰撞的組合,在使用者控制的指令碼ThirdPersonUserControl中只用到了Move方法來控制角色 
    [RequireComponent(typeof(Rigidbody))]
    [RequireComponent(typeof(CapsuleCollider))]
	[RequireComponent(typeof(Animator))]
	public class ThirdPersonCharacter : MonoBehaviour
	{                       
        /// <summary>
        /// 移動中轉向的速度
        /// </summary>
		[SerializeField] float m_MovingTurnSpeed = 360;//移動中轉向的速度
        /// <summary>
        /// 站立中轉向的速度
        /// </summary>
        [SerializeField] float m_StationaryTurnSpeed = 180;//站立中轉向的速度  
        /// <summary>
        /// 跳躍產生的力量
        /// </summary>
		[SerializeField] float m_JumpPower = 12f;//跳躍產生的力量  
        /// <summary>
        /// 重力
        /// </summary>
		[Range(1f, 4f)][SerializeField] float m_GravityMultiplier = 2f;//重力
        /// <summary>
        /// 腿偏移值
        /// </summary>
        [SerializeField] float m_RunCycleLegOffset = 0.2f; //腿偏移值  
   
        /// <summary>
        /// 移動速度
        /// </summary>
        [SerializeField]float m_MoveSpeedMultiplier = 1f;//移動速度
 
        /// <summary>
        /// 動畫播放速度
        /// </summary>
        [SerializeField] float m_AnimSpeedMultiplier = 1f;//動畫播放速度

        /// <summary>
        /// 地面檢查的距離
        /// </summary>
        [SerializeField] float m_GroundCheckDistance = 0.1f;//地面檢查的距離  
        /// <summary>
        /// 剛體
        /// </summary>
		Rigidbody m_Rigidbody;//剛體
        /// <summary>
        /// 動畫狀態機
        /// </summary>
		Animator m_Animator;//動畫狀態機
        /// <summary>
        /// 是否在地面上
        /// </summary>
		bool m_IsGrounded;//是否在地面上
        /// <summary>
        /// 地面距離檢測的起始值
        /// </summary>
        float m_OrigGroundCheckDistance;//地面距離檢測的起始值
        /// <summary>
        /// 一半
        /// </summary>
		const float k_Half = 0.5f;//一半
        /// <summary>
        /// 轉向值
        /// </summary>
        float m_TurnAmount;//轉向值
        /// <summary>
        /// 前進值
        /// </summary>
        float m_ForwardAmount;//前進值
        /// <summary>
        /// 地面法向量
        /// </summary>
		Vector3 m_GroundNormal;//地面法向量
        /// <summary>
        /// 膠囊高度
        /// </summary>
        float m_CapsuleHeight;//膠囊高度
        /// <summary>
        /// 膠囊的中心
        /// </summary>
        Vector3 m_CapsuleCenter;//膠囊的中心
        /// <summary>
        /// 膠囊體
        /// </summary>
		CapsuleCollider m_Capsule;//膠囊體
        /// <summary>
        /// 是否是蹲伏狀態
        /// </summary>
		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; //膠囊的中心

            //鎖定剛體的 XYZ軸的旋轉
			m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationY | RigidbodyConstraints.FreezeRotationZ;
            m_OrigGroundCheckDistance = m_GroundCheckDistance;//儲存一下地面檢查值  
		}




        // 這是在FixedUpdate中呼叫的Move方法  
        /// <summary>
        /// 移動方法
        /// </summary>
        /// <param name="move">移動的方向</param>
        /// <param name="crouch">是否蹲伏</param>
        /// <param name="jump">是否跳躍</param>
		public void Move(Vector3 move, bool crouch, bool jump)
		{
            // 將一個世界座標的輸入轉換為本地相關的轉向和前進速度,需要考慮到角色頭部的方向  
           


            if (move.magnitude > 1f) move.Normalize(); //向量大於1,則變為單位向量  

            move = transform.InverseTransformDirection(move);//將世界座標系的方向和位置轉換為自身座標系

            //判斷是否離開地面,
            //並修改:m_GroundNormal:地面法向量;
            //       m_IsGrounded //是否在地面上
            //       m_Animator.applyRootMotion//動畫是否影響實際位置
			CheckGroundStatus();

            // [Vector3.ProjectOnPlane]聖典:投影向量到一個平面上,該平面由垂直到該法線的平面定義。
            move = Vector3.ProjectOnPlane(move, m_GroundNormal);//根據地面的法向量,產生一個對應平面的速度方向.
            print("Move:" + move);

            //[Mathf.Atan2] :以弧度為單位計算並返回 y/x 的反正切值。返回值表示相對直角三角形對角的角,其中 x 是臨邊邊長,而 y 是對邊邊長。
            //返回值為x軸和一個零點起始在(x,y)結束的2D向量的之間夾角。
            m_TurnAmount = Mathf.Atan2(move.x, move.z);//產生一個方位角,即與z軸的夾角,用於人物轉向 
            print("Mathf.Atan2(move.x, move.z)用於人物轉向" + m_TurnAmount);


            m_ForwardAmount = move.z;//人物前進的數值

            //申請額外的旋轉轉
            ApplyExtraTurnRotation();//應用附加轉彎  

		
            // 控制和速度處理,在地上和空中是不一樣的  
			if (m_IsGrounded)//如果在地面上
			{
                //檢測是否能跳
				HandleGroundedMovement(crouch, jump);
			}
			else
			{
				HandleAirborneMovement();
			}
            //縮小膠囊體,並判斷是否可以站立
			ScaleCapsuleForCrouching(crouch);

            //在只能下蹲的區域保持下蹲
			PreventStandingInLowHeadroom();

            // 將輸入和其他狀態傳遞給動畫元件  
			UpdateAnimator(move);
		}

        /// <summary>
        /// 縮小膠囊碰撞體
        /// </summary>
        /// <param name="crouch">是否蹲伏</param>
		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, Physics.AllLayers, QueryTriggerInteraction.Ignore))
                { //這邊的意思是從角色底部向上丟一個球,然後那個k_Half相關的引數是為了放置在丟的時候就碰到了地面,而做的向上偏移  

                    m_Crouching = true; //碰到了,說明角色無法回到站立狀態  
					return;
				}
                // 沒有碰到,回到初始的狀態  
				m_Capsule.height = m_CapsuleHeight;
				m_Capsule.center = m_CapsuleCenter;
				m_Crouching = false;
			}
		}




        /// <summary>
        /// 在只能下蹲的區域保持下蹲  
        /// </summary>
		void PreventStandingInLowHeadroom()
		{
            // 在只能下蹲的區域保持下蹲  
			if (!m_Crouching)
            {                                                                       //radius半徑
				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;//掃描的長度
                //當球形掃描與任意碰撞器相交,返回true;否則返回false。   QueryTriggerInteraction:指定是否查詢碰到觸發器
				if (Physics.SphereCast(crouchRay, m_Capsule.radius * k_Half, crouchRayLength, Physics.AllLayers, QueryTriggerInteraction.Ignore))
				{
					m_Crouching = true;//下蹲
				}
			}
		}



        /// <summary>
        /// 用來更新動畫狀態機裡的值
        /// </summary>
        /// <param name="move">移動引數</param>
		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(//獲取當前是在哪個腳,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);
			}

            // 這邊的方法允許我們在inspector檢視中調整動畫的速率,他會因為根運動影響移動的速度  
			if (m_IsGrounded && move.magnitude > 0)
			{
				m_Animator.speed = m_AnimSpeedMultiplier;
			}
			else
			{
                // 在空中的時候不用  
				m_Animator.speed = 1;
			}
		}

        /// <summary>
        /// 空中的處理,注意,空中跳躍和下蹲時不起作用的
        /// </summary>
		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;//上升的時候不判斷是否在地面上  
		}

        /// <summary>
        /// 跳躍方法,檢測是否能跳
        /// </summary>
        /// <param name="crouch">是否蹲伏</param>
        /// <param name="jump">是否跳躍</param>
		void HandleGroundedMovement(bool crouch, bool jump)
		{
			
            //檢查是否允許跳條件是正確的
             //m_Animator.GetCurrentAnimatorStateInfo(0)獲取當前動畫狀態資訊
			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;//是否跳躍為false
				m_Animator.applyRootMotion = false;//設定動畫不影響實際位置
				m_GroundCheckDistance = 0.1f;//檢測地面距離0.1
                print(m_GroundCheckDistance);
			}
		}

       /// <summary>
       /// 幫助角色快速轉向,這是動畫中根旋轉的附加項  
       /// </summary>
		void ApplyExtraTurnRotation()
		{
            //幫助這個角色將更快(這是除了根旋轉的動畫)
            // 幫助角色快速轉向,這是動畫中根旋轉的附加項  
			float turnSpeed = Mathf.Lerp(m_StationaryTurnSpeed, m_MovingTurnSpeed, m_ForwardAmount);
            transform.Rotate(0, m_TurnAmount * turnSpeed * Time.deltaTime, 0);//轉向.  
		}

        /// <summary>
        /// 這個方法來覆蓋預設的根運動,這個方法允許我們移除位置的速度  
        /// </summary>
		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;
			}
		}

        /// <summary>
        /// 檢測是否離開地面
        /// </summary>
		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))
            {//射到了,儲存法向量,改變變數,將動畫的applyRootMotion置為true,true的含義是應用骨骼節點的位移
             //就是說動畫的運動會對實際角色座標產生影響,用於精確的播放動畫  
                m_GroundNormal = hitInfo.normal;//將射線觸碰到的物體的法向量賦值給m_GroundNormal
				m_IsGrounded = true;//是否在地面上
				m_Animator.applyRootMotion = true;//動畫影響實際位置
			}
			else
			{
                m_IsGrounded = false;//是否在地面上為false
				m_GroundNormal = Vector3.up;
                m_Animator.applyRootMotion = false;//動畫不影響實際位置,不過我感覺這邊不設為false也是可以的  
			}
		}
	}
}