Unity官方实例ThirdPersonController讲解

// 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<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;
        }

        /// <summary>
        /// 供调用的角色运动主方法
        /// </summary>
        /// <param name="move"></param>
        /// <param name="crouch"></param>
        /// <param name="jump"></param>
        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);
        }

        /// <summary>
        /// 处理下蹲状态的胶囊体缩放
        /// </summary>
        /// <param name="crouch"></param>
        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;
            }
        }

        /// <summary>
        /// 一开始没理解 其实这里要表达的意思是当有障碍物主动的接近角色上方时 角色会下蹲躲避
        /// </summary>
        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;
                }
            }
        }

        /// <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);
            // 如果角色没有着地 将它竖直方向的刚体速度赋值给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;
            }
        }

        /// <summary>
        /// 处理角色下落速度和射线检测赋值
        /// </summary>
        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;
        }

        /// <summary>
        /// 处理角色着地时运动状态
        /// </summary>
        /// <param name="crouch"></param>
        /// <param name="jump"></param>
        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;
            }
        }

        /// <summary>
        /// 赋予角色在不同运动状态下的不同转向速度
        /// </summary>
        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;
            }
        }

        /// <summary>
        /// 检测与地面距离
        /// </summary>
        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;                      // 是否起跳

        /// <summary>
        /// 找到主摄像机和角色控制脚本
        /// </summary>
        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<ThirdPersonCharacter>();
        }

        /// <summary>
        /// 只处理是否起跳
        /// </summary>
        private void Update()
        {
            if (!m_Jump)
            {
                // 是否起跳和是否悬空不同 这里的是否起跳是个瞬时状态 只有当起跳键被按下的瞬间m_Jump返回true
                m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");
            }
        }

        /// <summary>
        /// 物理运动相关的要放在FixedUpdate里
        /// </summary>
        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;
        }
    }
}
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值