gogoDragon

导航

Unity自学--CreatorKit代码解析 1

Unity初学者项目 Creator Kit Beginner

ExampleScene 样例场景如下

 1.附着脚本:(Character)

Character对象是游戏项目的PLayer,即玩家操纵的游戏对象,对于Character来说,需要拥有自己的运行脚本。

附着在Character对象上的运行脚本有如下:

 

 可以看到,除了基础的Transform模块外还存在CapsuleCollider 用于检测碰撞,NavMeshAgent用于自动寻路,Rigidbody用于描述物体的物理性质,官方文档有详细描述。

2.CharacterControl 代码解析

using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Timers;
using CreatorKitCode;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;

namespace CreatorKitCodeInternal {
    //继承了基类,实现了攻击,移动的接口
    public class CharacterControl : MonoBehaviour, 
        AnimationControllerDispatcher.IAttackFrameReceiver,
        AnimationControllerDispatcher.IFootstepFrameReceiver
    {
        //单例对象
        public static CharacterControl Instance { get; protected set; }
    
        //移动速度
        public float Speed = 10.0f;

        //角色数据 => 依赖于m_CharacterData 即附着在Character中的CharacterData脚本类
        public CharacterData Data => m_CharacterData;
        //目标数据 => 依赖于m_CurrentTargetCharacterData 即附着在其他对象中的CharacterData脚本类
        public CharacterData CurrentTarget => m_CurrentTargetCharacterData;

        //武器的位置数据等
        public Transform WeaponLocator;
    
        //[Header] 显示标头 在Unity中显示Audio
        [Header("Audio")]
        //音频数据
        public AudioClip[] SpurSoundClips;
    
        //最后射线的位置
        Vector3 m_LastRaycastResult;
        //动画播放器
        Animator m_Animator;
        //寻路器
        NavMeshAgent m_Agent;
        //用户数据
        CharacterData m_CharacterData;

        //高光类,用于高光渲染
        HighlightableObject m_Highlighted;

        //RaycastHit用于存储RayCast(射线)命中的数据
        RaycastHit[] m_RaycastHitCache = new RaycastHit[16];

        int m_SpeedParamID;
        int m_AttackParamID;
        int m_HitParamID;
        //眩晕id
        int m_FaintParamID;
        //复位,出生ID
        int m_RespawnParamID;

        bool m_IsKO = false;
        float m_KOTimer = 0.0f;

        //交互层级--层级
        int m_InteractableLayer;
        int m_LevelLayer;
        //碰撞器
        Collider m_TargetCollider;
        //交互对象
        InteractableObject m_TargetInteractable = null;
        //主视角
        Camera m_MainCamera;
        //自动寻路组件产生的路径,保存在corners中
        NavMeshPath m_CalculatedPath;
        //主角音频
        CharacterAudio m_CharacterAudio;
        //目标层 当前目标数据
        int m_TargetLayer;
        CharacterData m_CurrentTargetCharacterData = null;
        //this is a flag that tell the controller it need to clear the target once the attack finished.
        //usefull for when clicking elwswhere mid attack animation, allow to finish the attack and then exit.
        //当攻击完成后需要清除的标志位,用于攻击动画途中点击某个位置时允许退出攻击状态并离开
        bool m_ClearPostAttack = false;

        //复生点,当经过存在复生点的区域时,设置复生点
        SpawnPoint m_CurrentSpawn = null;
    
        //枚举状态 默认 受击 攻击
        enum State
        {
            DEFAULT,
            HIT,
            ATTACKING
        }

        //当前状态
        State m_CurrentState;

        //初始化实例 获得主摄像机 Camera.main tag标签为MainCamera的摄像机
        void Awake()
        {
            Instance = this;
            m_MainCamera = Camera.main;
        }

        // Start is called before the first frame update
        void Start()
        {
            //QualitySettings品质接口 此处为不等待垂直同步 (垂直同步防止跳帧,撕裂(关闭会更流畅))
            QualitySettings.vSyncCount = 0;
            //尝试60fps 设置了QualitySettings.vSyncCount之后,targetFrameRate不使用,使用平台默认刷新率
            Application.targetFrameRate = 60;
        
            //新建路径
            m_CalculatedPath = new NavMeshPath();
        
            //获取寻路组件,动画组件
            m_Agent = GetComponent<NavMeshAgent>();
            m_Animator = GetComponentInChildren<Animator>();
        
            //寻路的速度设为当前移动速度
            m_Agent.speed = Speed;
            //设置最大回转速度 需要转弯的速度
            m_Agent.angularSpeed = 360.0f;

            //射线位置设为当前位置
            m_LastRaycastResult = transform.position;

            //初始化动画ID
            m_SpeedParamID = Animator.StringToHash("Speed");
            m_AttackParamID = Animator.StringToHash("Attack");
            m_HitParamID = Animator.StringToHash("Hit");
            m_FaintParamID = Animator.StringToHash("Faint");
            m_RespawnParamID = Animator.StringToHash("Respawn");

            //获得人物数据
            m_CharacterData = GetComponent<CharacterData>();

            //给人物数据中的武器装备动作提供
            m_CharacterData.Equipment.OnEquiped += item =>
            {
                if (item.Slot == (EquipmentItem.EquipmentSlot)666)
                {
                    //初始化预制件,父类是当前对象,不在世界坐标系下生成(父类坐标系下生成)
                    var obj = Instantiate(item.WorldObjectPrefab, WeaponLocator, false);
                    //将每一层都变为PlayerEquipment层循环调用
                    Helpers.RecursiveLayerChange(obj.transform, LayerMask.NameToLayer("PlayerEquipment"));
                }
            };
        
            m_CharacterData.Equipment.OnUnequip += item =>
            {
                if (item.Slot == (EquipmentItem.EquipmentSlot)666)
                {
                    //摧毁所有的角色
                    foreach(Transform t in WeaponLocator)
                        Destroy(t.gameObject);
                }
            };
            
            //角色数据初始化
            m_CharacterData.Init();
        
            //获得层级位置
            m_InteractableLayer = 1 << LayerMask.NameToLayer("Interactable");
            m_LevelLayer = 1 << LayerMask.NameToLayer("Level");
            m_TargetLayer = 1 << LayerMask.NameToLayer("Target");

            //设置默认状态
            m_CurrentState = State.DEFAULT;

            //设置音频
            m_CharacterAudio = GetComponent<CharacterAudio>();
        
            //受伤时的动作函数 (设置动画,调用Hit函数播放)
            m_CharacterData.OnDamage += () =>
            {
                m_Animator.SetTrigger(m_HitParamID);
                m_CharacterAudio.Hit(transform.position);
            };
        }

        // Update is called once per frame
        void Update()
        {

            Vector3 pos = transform.position;

            //判断是否死了,死了设置复活时间m_KOTimer,达到3s后调用GoToRespawn()复活
            if (m_IsKO)
            {
                m_KOTimer += Time.deltaTime;
                if (m_KOTimer > 3.0f)
                {
                    GoToRespawn();
                }

                return;
            }

            //The update need to run, so we can check the health here.
            //Another method would be to add a callback in the CharacterData that get called
            //when health reach 0, and this class register to the callback in Start
            //(see CharacterData.OnDamage for an example)
            //当前生命值为 0时死亡
            if (m_CharacterData.Stats.CurrentHealth == 0)
            {
                //设置眩晕状态
                m_Animator.SetTrigger(m_FaintParamID);

                //寻路停止
                m_Agent.isStopped = true;
                //清空路径
                m_Agent.ResetPath();
                //状态值为死亡
                m_IsKO = true;
                //设置复活计时器
                m_KOTimer = 0.0f;
            
                //调用死亡函数
                Data.Death();
                
                //调用死亡音频
                m_CharacterAudio.Death(pos);
            
                return;
            }
            
            //没死亡的话 获取在鼠标位置射线
            Ray screenRay = CameraController.Instance.GameplayCamera.ScreenPointToRay(Input.mousePosition);
        
            //交互对象不为空 ,进入交互状态
            if (m_TargetInteractable != null)
            {
                CheckInteractableRange();
            }

            //目标数据不为空 判断对象是否死亡,是置为空,否则进入攻击状态
            if (m_CurrentTargetCharacterData != null)
            {
                if (m_CurrentTargetCharacterData.Stats.CurrentHealth == 0)
                    m_CurrentTargetCharacterData = null;
                else
                    CheckAttack();
            }
        
            //鼠标滚轮事件
            float mouseWheel = Input.GetAxis("Mouse ScrollWheel");
            //如果滚动了
            if (!Mathf.Approximately(mouseWheel, 0.0f))
            {
                //获取当前的鼠标位置(从屏幕空间变换为视口空间)
                Vector3 view = m_MainCamera.ScreenToViewportPoint(Input.mousePosition);
                //如果处在摄像机范围内
                if(view.x > 0f && view.x < 1f && view.y > 0f && view.y < 1f)
                    //变换摄像机视角
                    CameraController.Instance.Zoom(-mouseWheel * Time.deltaTime * 20.0f);
            }
        
            if(Input.GetMouseButtonDown(0))
            { //if we click the mouse button, we clear any previously et targets

                //按下按键但不是攻击状态时,清空所有的对象(攻击对象,交互对象) 否则攻击后清除
                if (m_CurrentState != State.ATTACKING)
                {
                    m_CurrentTargetCharacterData = null;
                    m_TargetInteractable = null;
                }
                else
                {
                    //处于攻击状态时
                    m_ClearPostAttack = true;
                }
            }

            //EventSystem.current.IsPointerOverGameObject()判断触点是否在对象上而不是UI上
            if (!EventSystem.current.IsPointerOverGameObject() && m_CurrentState != State.ATTACKING)
            {
                //Raycast to find object currently under the mouse cursor
                //用射线的方式获取光标位置下的对象
                //设置为高光对象 没有 则不设置
                ObjectsRaycasts(screenRay);
            
                if (Input.GetMouseButton(0))
                {
                    //如果按下按键
                    if (m_TargetInteractable == null && m_CurrentTargetCharacterData == null)
                    {
                        //判断高光对象
                        InteractableObject obj = m_Highlighted as InteractableObject;
                        if (obj)
                        {
                            InteractWith(obj);
                        }
                        else
                        {
                            CharacterData data = m_Highlighted as CharacterData;
                            if (data != null)
                            {
                                m_CurrentTargetCharacterData = data;
                            }
                            else
                            {
                                //高光对象也为NULL则寻路检查
                                MoveCheck(screenRay);
                            }
                        }
                    }
                }
            }

            //设置平滑的移动
            m_Animator.SetFloat(m_SpeedParamID, m_Agent.velocity.magnitude / m_Agent.speed);
        
            //Keyboard shortcuts
            //按I键获取物品栏
            if(Input.GetKeyUp(KeyCode.I))
                UISystem.Instance.ToggleInventory();
        }

        void GoToRespawn()
        {
            m_Animator.ResetTrigger(m_HitParamID);
        
            //设置代理路径
            m_Agent.Warp(m_CurrentSpawn.transform.position);
            //停止寻路
            m_Agent.isStopped = true;
            //清空路径
            m_Agent.ResetPath();
            //复活
            m_IsKO = false;

            //所有状态重置
            m_CurrentTargetCharacterData = null;
            m_TargetInteractable = null;

            m_CurrentState = State.DEFAULT;
        
            m_Animator.SetTrigger(m_RespawnParamID);

            //恢复状态
            m_CharacterData.Stats.ChangeHealth(m_CharacterData.Stats.stats.health);
        }

        void ObjectsRaycasts(Ray screenRay)
        {
            //bool值
            bool somethingFound = false;

            //first check for interactable Object
            //往screenRay方向投射球体,半径为1,m_RaycastHitCache存储命中对象,m_InteractableLayer遮罩层忽略不在层中的对象
            int count = Physics.SphereCastNonAlloc(screenRay, 1.0f, m_RaycastHitCache, 1000.0f, m_InteractableLayer);
            //缓冲区个数不为0时
            if (count > 0)
            {
                for (int i = 0; i < count; ++i)
                {
                    //获取每个碰撞体中的可交互对象(附着体)
                    InteractableObject obj = m_RaycastHitCache[0].collider.GetComponentInParent<InteractableObject>();
                    //不为NULL且可交互时
                    if (obj != null && obj.IsInteractable)
                    {
                        //调用函数使物体高光
                        SwitchHighlightedObject(obj);
                        //设置为找到东西,跳出循环
                        somethingFound = true;
                        break;
                    }
                }
            }
            else
            {
                //往screenRay方向投射球体,半径为1,m_RaycastHitCache存储命中对象,m_InteractableLayer遮罩层忽略不在层中的对象
                count = Physics.SphereCastNonAlloc(screenRay, 1.0f, m_RaycastHitCache, 1000.0f, m_TargetLayer);

                if (count > 0)
                {
                    CharacterData data = m_RaycastHitCache[0].collider.GetComponentInParent<CharacterData>();
                    //数据不为null时
                    if (data != null)
                    {
                        SwitchHighlightedObject(data);
                        somethingFound = true;
                    }
                }
            }

            //没找到对象并且高光对象不为NULL时
            if (!somethingFound && m_Highlighted != null)
            {
                //清除高光对象
                SwitchHighlightedObject(null);
            }
        }

        void SwitchHighlightedObject(HighlightableObject obj)
        {
            //高光对象不为null时 取消高光
            if(m_Highlighted != null) m_Highlighted.Dehighlight();

            //设置高光对象
            m_Highlighted = obj;
            //高光对象不为null时 设置高光
            if (m_Highlighted != null) m_Highlighted.Highlight();
        }

        //移动检测
        void MoveCheck(Ray screenRay)
        {
            //判断寻路状态是否在目的地终止 NavMeshPathStatus.PathComplete
            if ( m_CalculatedPath.status == NavMeshPathStatus.PathComplete)
            {
                //设置新路径
                m_Agent.SetPath(m_CalculatedPath);
                //清除路径
                m_CalculatedPath.ClearCorners();
            }
        
            if (Physics.RaycastNonAlloc(screenRay, m_RaycastHitCache, 1000.0f, m_LevelLayer) > 0)
            {
                //抛射对象大于0时,点击位置的对象,选择碰到的第一个
                //获取第一个对象
                Vector3 point = m_RaycastHitCache[0].point;
                //avoid recomputing path for close enough click
                //避免重复计算近距离点击(点在上次点的位置附近的就不移动了)
                if (Vector3.SqrMagnitude(point - m_LastRaycastResult) > 1.0f)
                {
                    NavMeshHit hit;
                    //NavMesh.SamplePosition判断是否是可行区域  point 目标点 hit 输出最近路径 NavMesh.AllAreas全路段
                    if (NavMesh.SamplePosition(point, out hit, 0.5f, NavMesh.AllAreas))
                    {//sample just around where we hit, avoid setting destination outside of navmesh (ie. on building)
                        //是可行区域设置之前的位置
                        m_LastRaycastResult = point;
                        //m_Agent.SetDestination(hit.position);

                        //计算路径并将其储存到m_CalculatedPath中
                        m_Agent.CalculatePath(hit.position, m_CalculatedPath);
                    }
                }
            }
        }

        //交互
        void CheckInteractableRange()
        {
            //攻击状态,则不交互
            if(m_CurrentState == State.ATTACKING)
                return;

            //ClosestPointOnBounds与目标碰撞体碰撞的最近的位置  相减得出距离
            Vector3 distance = m_TargetCollider.ClosestPointOnBounds(transform.position) - transform.position;
        
            
            //距离小于一个范围
            if (distance.sqrMagnitude < 1.5f * 1.5f)
            {
                //停止寻路
                StopAgent();
                //交互
                m_TargetInteractable.InteractWith(m_CharacterData);
                //交互完了,交互对象为NULL,再次点击才会继续交互
                m_TargetInteractable = null;
            }
        }

        //停止寻路
        void StopAgent()
        {
            m_Agent.ResetPath();
            //速度为0 立刻停止
            m_Agent.velocity = Vector3.zero;
        }

        void CheckAttack()
        {
            //处于攻击状态,返回
            if(m_CurrentState == State.ATTACKING)
                return;
               
            //是否达到攻击范围
            if (m_CharacterData.CanAttackReach(m_CurrentTargetCharacterData))
            {
                //达到,停止寻路
                StopAgent();

                //if the mouse button isn't pressed, we do NOT attack
                //判断是否按下攻击键
                if (Input.GetMouseButton(0))
                {
                    //获得两者的距离
                    Vector3 forward = (m_CurrentTargetCharacterData.transform.position - transform.position);
                    //垂直距离为0
                    forward.y = 0;
                    //归一化
                    forward.Normalize();

                    //设置标准化矢量
                    transform.forward = forward;
                    //判断是否能够攻击到目标
                    if (m_CharacterData.CanAttackTarget(m_CurrentTargetCharacterData))
                    {
                        //能则进入攻击状态
                        m_CurrentState = State.ATTACKING;

                        //播放动画和音效
                        m_CharacterData.AttackTriggered();
                        m_Animator.SetTrigger(m_AttackParamID);
                    }
                }
            }
            else
            {
                //没达到则设置攻击目标的位置为寻路位置
                m_Agent.SetDestination(m_CurrentTargetCharacterData.transform.position);
            }
        }

        public void AttackFrame()
        {
            //攻击帧 攻击人物不存在了 放弃攻击 返回
            if (m_CurrentTargetCharacterData == null)
            {
                m_ClearPostAttack = false;
                return;
            }

            //if we can't reach the target anymore when it's time to damage, then that attack miss.
            //攻击范围是否达到了
            if (m_CharacterData.CanAttackReach(m_CurrentTargetCharacterData))
            {
                //攻击
                m_CharacterData.Attack(m_CurrentTargetCharacterData);
                //设置攻击位置和播放动画和音频
                var attackPos = m_CurrentTargetCharacterData.transform.position + transform.up * 0.5f;
                VFXManager.PlayVFX(VFXType.Hit, attackPos);
                SFXManager.PlaySound(m_CharacterAudio.UseType, new SFXManager.PlayData() { Clip = m_CharacterData.Equipment.Weapon.GetHitSound(), PitchMin = 0.8f, PitchMax = 1.2f, Position = attackPos });
            }

            //攻击过了
            if(m_ClearPostAttack)
            {
                //清空所有对象
                m_ClearPostAttack = false;
                m_CurrentTargetCharacterData = null;
                m_TargetInteractable = null;
            }
            //恢复默认状态
            m_CurrentState = State.DEFAULT;
        }

        //设置出生点
        public void SetNewRespawn(SpawnPoint point)
        {
            //设置为死寂状态(enable属性为可用)
            if(m_CurrentSpawn != null)
                m_CurrentSpawn.Deactivated();

            m_CurrentSpawn = point;
            //设置为活跃状态
            m_CurrentSpawn.Activated();
        }

        //交互函数
        public void InteractWith(InteractableObject obj)
        {
            if (obj.IsInteractable)
            {
                //可交互则获得碰撞器
                m_TargetCollider = obj.GetComponentInChildren<Collider>();
                //获得交互对象
                m_TargetInteractable = obj;
                //设置寻路目标
                m_Agent.SetDestination(obj.transform.position);
            }
        }

        //实现的接口函数
        public void FootstepFrame()
        {
            Vector3 pos = transform.position;
        
            m_CharacterAudio.Step(pos);
        
            SFXManager.PlaySound(SFXManager.Use.Player, new SFXManager.PlayData()
            {
                Clip = SpurSoundClips[Random.Range(0, SpurSoundClips.Length)], 
                Position = pos,
                PitchMin = 0.8f,
                PitchMax = 1.2f,
                Volume = 0.3f
            });
        
            VFXManager.PlayVFX(VFXType.StepPuff, pos);  
        }
    }
}

3.整体人物逻辑设计(Update逻辑详解)

 

posted on 2022-04-05 19:36  我不知道学些啥  阅读(219)  评论(0编辑  收藏  举报