Unity教学项目Ceator Kit:FPS 源代码学习笔记(二)Weapon类

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class Weapon : MonoBehaviour
{
    static RaycastHit[] s_HitInfoBuffer = new RaycastHit[8];//受击射线
    
    public enum TriggerType//碰触类型
    {
        Auto,//自动
        Manual//手动
    }

    public enum WeaponType//武器类型
    {
        Raycast,//射线
        Projectile//投掷物
    }

    public enum WeaponState//武器状态
    {
        Idle,//等待
        Firing,//开火
        Reloading//重填
    }

    [System.Serializable]
    public class AdvancedSettings//高级设置
    {
        public float spreadAngle = 0.0f;//传播角度
        public int projectilePerShot = 1;//射弹
        public float screenShakeMultiplier = 1.0f;//屏幕抖动倍增器
    }

    public TriggerType triggerType = TriggerType.Manual;//触碰类型初始化为手动
    public WeaponType weaponType = WeaponType.Raycast;//武器类型初始化为射线
    public float fireRate = 0.5f;//开火频率
    public float reloadTime = 2.0f;//装弹时间
    public int clipSize = 4;//剪辑大小,音频
    public float damage = 1.0f;//伤害

    [AmmoType]
    public int ammoType = -1;//弹药类型

    public Projectile projectilePrefab;//投掷物预制体
    public float projectileLaunchForce = 200.0f;//投掷物发射力

    public Transform EndPoint; //结束点

    public AdvancedSettings advancedSettings;//高级设置
    
    [Header("Animation Clips")]//面板显示时给予一个开头的标识
    public AnimationClip FireAnimationClip;//开火动画音频
    public AnimationClip ReloadAnimationClip;//装弹动画音频

    [Header("Audio Clips")]
    public AudioClip FireAudioClip;//开火音频
    public AudioClip ReloadAudioClip;//装弹音频
    
    [Header("Visual Settings")]
    public LineRenderer PrefabRayTrail;//预制射线追踪

    [Header("Visual Display")]
    public AmmoDisplay AmmoDisplay;//弹药显示

    public bool triggerDown//是否碰触
    {
        get { return m_TriggerDown; }
        set 
        { 
            m_TriggerDown = value;
            if (!m_TriggerDown) m_ShotDone = false;
        }
    }

    public WeaponState CurrentState => m_CurrentState;//当前状态,=>不知道是什么意思
    public int ClipContent => m_ClipContent;//音频内容
    public Controller Owner => m_Owner;//所有者

    Controller m_Owner;//所有者
    
    Animator m_Animator;//动画管理器
    WeaponState m_CurrentState;//武器状态
    bool m_ShotDone;//射击完成
    float m_ShotTimer = -1.0f;//射击计时器
    bool m_TriggerDown;//触碰
    int m_ClipContent;//自身音频的内容

    AudioSource m_Source;//资源

    Vector3 m_ConvertedMuzzlePos;//转换后枪口的位置

    class ActiveTrail//射击光线的拖尾
    {
        public LineRenderer renderer;//光线渲染
        public Vector3 direction;//方向
        public float remainingTime;//持续时间
    }
    
    List<ActiveTrail> m_ActiveTrails = new List<ActiveTrail>();//光线拖尾列表
    
    Queue<Projectile> m_ProjectilePool = new Queue<Projectile>();//投掷物
    
    int fireNameHash = Animator.StringToHash("fire");//开火
    int reloadNameHash = Animator.StringToHash("reload");//装弹   

    void Awake()
    {
        m_Animator = GetComponentInChildren<Animator>();//获取到当前位置孩子中的动画控制器
        m_Source = GetComponentInChildren<AudioSource>();//获取音频资源器
        m_ClipContent = clipSize;//音频大小

        if (PrefabRayTrail != null)//拖尾预制体不为空
        {
            const int trailPoolSize = 16;//拖尾池大小
            PoolSystem.Instance.InitPool(PrefabRayTrail, trailPoolSize);//初始化对象池
        }

        if (projectilePrefab != null)//投掷物预制体不为空
        {
            //a minimum of 4 is useful for weapon that have a clip size of 1 and where you can throw a second
            //or more before the previous one was recycled/exploded.
            int size = Mathf.Max(4, clipSize) * advancedSettings.projectilePerShot;//控制投掷物体多少
            for (int i = 0; i < size; ++i)
            {
                Projectile p = Instantiate(projectilePrefab);//实例化
                p.gameObject.SetActive(false);//隐藏
                m_ProjectilePool.Enqueue(p);//退出队列
            }
        }
    }

    public void PickedUp(Controller c)//指定当前武器归属权
    {
        m_Owner = c;
    }

    public void PutAway()//
    {
        m_Animator.WriteDefaultValues();//为动画器写入默认值
        
        for (int i = 0; i < m_ActiveTrails.Count; ++i)//制造拖尾并隐藏
        {
            var activeTrail = m_ActiveTrails[i];
            m_ActiveTrails[i].renderer.gameObject.SetActive(false);
        }
        
        m_ActiveTrails.Clear();//清空
    }

    public void Selected()//被选中
    {
        var ammoRemaining = m_Owner.GetAmmo(ammoType);//获取仓库中的弹药量
        
        //gun get disabled when ammo is == 0 and there is no more ammo in the clip, so this allow to re-enable it if we
        //grabbed ammo since last time we switched
        gameObject.SetActive(ammoRemaining != 0 || m_ClipContent != 0);//只要还有弹药或者音频内容不为0,就一直显示
        //设置动画
        if (FireAnimationClip != null)
            m_Animator.SetFloat("fireSpeed",  FireAnimationClip.length / fireRate);
        
        if(ReloadAnimationClip != null)
            m_Animator.SetFloat("reloadSpeed", ReloadAnimationClip.length / reloadTime);
        
        m_CurrentState = WeaponState.Idle;//当前状态为等待或者说初始

        triggerDown = false;//初始化
        m_ShotDone = false;//射击完成
        
        WeaponInfoUI.Instance.UpdateWeaponName(this);//刷新当前武器名称
        WeaponInfoUI.Instance.UpdateClipInfo(this);//刷新射击声音
        WeaponInfoUI.Instance.UpdateAmmoAmount(m_Owner.GetAmmo(ammoType));//刷新弹药量
        
        if(AmmoDisplay)//弹药显示
            AmmoDisplay.UpdateAmount(m_ClipContent, clipSize);

        if (m_ClipContent == 0 && ammoRemaining != 0)//装弹的时候弹药储备不能为空,同时音频内容为空
        { 
            //this can only happen if the weapon ammo reserve was empty and we picked some since then. So directly
            //reload the clip when wepaon is selected          
            int chargeInClip = Mathf.Min(ammoRemaining, clipSize);//
            m_ClipContent += chargeInClip;        
            if(AmmoDisplay)//显示弹药
                AmmoDisplay.UpdateAmount(m_ClipContent, clipSize);        
            m_Owner.ChangeAmmo(ammoType, -chargeInClip);       //改变弹药
            WeaponInfoUI.Instance.UpdateClipInfo(this);//刷新音频
        }
        
        m_Animator.SetTrigger("selected");//设置为被选中
    }

    public void Fire()//开火
    {
        if (m_CurrentState != WeaponState.Idle || m_ShotTimer > 0 || m_ClipContent == 0)//状态必须是等待状态,射击计时器大于0,音频内容等于0
            return;
        
        m_ClipContent -= 1;
        
        m_ShotTimer = fireRate;//计时器等于开火频率

        if(AmmoDisplay)//弹药显示
            AmmoDisplay.UpdateAmount(m_ClipContent, clipSize);
        
        WeaponInfoUI.Instance.UpdateClipInfo(this);//刷新音频

        //the state will only change next frame, so we set it right now.
        m_CurrentState = WeaponState.Firing;//更新状态
        
        m_Animator.SetTrigger("fire");//设置触发器为开火

        m_Source.pitch = Random.Range(0.7f, 1.0f);//随机音调
        m_Source.PlayOneShot(FireAudioClip);//播放开火音效
        
        CameraShaker.Instance.Shake(0.2f, 0.05f * advancedSettings.screenShakeMultiplier);//摄像机抖动

        if (weaponType == WeaponType.Raycast)//判断射击方式
        {
            for (int i = 0; i < advancedSettings.projectilePerShot; ++i)
            {
                RaycastShot();
            }
        }
        else
        {
            ProjectileShot();
        }
    }


    void RaycastShot()//射线射击
    {

        //compute the ratio of our spread angle over the fov to know in viewport space what is the possible offset from center
        float spreadRatio = advancedSettings.spreadAngle / Controller.Instance.MainCamera.fieldOfView;//传播角度

        Vector2 spread = spreadRatio * Random.insideUnitCircle;//传播
        
        RaycastHit hit;
        Ray r = Controller.Instance.MainCamera.ViewportPointToRay(Vector3.one * 0.5f + (Vector3)spread);//返回从相机出发穿过视点的一射线
        Vector3 hitPosition = r.origin + r.direction * 200.0f;//被接触位置
        
        if (Physics.Raycast(r, out hit, 1000.0f, ~(1 << 9), QueryTriggerInteraction.Ignore))//武器layer
        {
            Renderer renderer = hit.collider.GetComponentInChildren<Renderer>();//获取到被接触物体下的渲染
            ImpactManager.Instance.PlayImpact(hit.point, hit.normal, renderer == null ? null : renderer.sharedMaterial);

            //if too close, the trail effect would look weird if it arced to hit the wall, so only correct it if far
            if (hit.distance > 5.0f)
                hitPosition = hit.point;
            
            //this is a target
            if (hit.collider.gameObject.layer == 10)//如果被碰触物体是第十层
            {
                Target target = hit.collider.gameObject.GetComponent<Target>();//就获取其目标组件
                target.Got(damage);//给予伤害
            }
        }


        if (PrefabRayTrail != null)//预制体拖尾存在
        {
            var pos = new Vector3[] { GetCorrectedMuzzlePlace(), hitPosition };
            var trail = PoolSystem.Instance.GetInstance<LineRenderer>(PrefabRayTrail);//获取拖尾对象池中的对象
            trail.gameObject.SetActive(true);//显示拖尾
            trail.SetPositions(pos);//设置位置
            m_ActiveTrails.Add(new ActiveTrail()
            {
                remainingTime = 0.3f,//维系时间
                direction = (pos[1] - pos[0]).normalized,//方向
                renderer = trail//渲染
            });
        }
    }

    void ProjectileShot()//投掷
    {
        for (int i = 0; i < advancedSettings.projectilePerShot; ++i)
        {
            float angle = Random.Range(0.0f, advancedSettings.spreadAngle * 0.5f);//角度
            Vector2 angleDir = Random.insideUnitCircle * Mathf.Tan(angle * Mathf.Deg2Rad);//角度方向

            Vector3 dir = EndPoint.transform.forward + (Vector3)angleDir;//目标方向和角度方向之和
            dir.Normalize();//归一化

            var p = m_ProjectilePool.Dequeue();//对象池操作
            
            p.gameObject.SetActive(true);
            p.Launch(this, dir, projectileLaunchForce);
        }
    }

    //For optimization, when a projectile is "destroyed" it is instead disabled and return to the weapon for reuse.
    public void ReturnProjecticle(Projectile p)//增加投掷物
    {
        m_ProjectilePool.Enqueue(p);
    }

    public void Reload()//重装弹药
    {
        if (m_CurrentState != WeaponState.Idle || m_ClipContent == clipSize)//判断状态
            return;

        int remainingBullet = m_Owner.GetAmmo(ammoType);//获取弹药量

        if (remainingBullet == 0)//如果没有弹药了,就隐藏武器
        {
            //No more bullet, so we disable the gun so it's not displayed anymore and change weapon
            gameObject.SetActive(false);
            return;
        }


        if (ReloadAudioClip != null)//装弹声音不为空,就随机音量,播放以此
        {
            m_Source.pitch = Random.Range(0.7f, 1.0f);
            m_Source.PlayOneShot(ReloadAudioClip);
        }

        int chargeInClip = Mathf.Min(remainingBullet, clipSize - m_ClipContent);
     
        //the state will only change next frame, so we set it right now.
        m_CurrentState = WeaponState.Reloading;//状态
        
        m_ClipContent += chargeInClip;
        
        if(AmmoDisplay)//弹药显示刷新
            AmmoDisplay.UpdateAmount(m_ClipContent, clipSize);
        
        m_Animator.SetTrigger("reload");//动画
        
        m_Owner.ChangeAmmo(ammoType, -chargeInClip);//改变弹药
        
        WeaponInfoUI.Instance.UpdateClipInfo(this);//刷新武器UI
    }

    void Update()
    {
        UpdateControllerState(); //刷新控制器状态       
        
        if (m_ShotTimer > 0)//射击计时器
            m_ShotTimer -= Time.deltaTime;

        Vector3[] pos = new Vector3[2];
        for (int i = 0; i < m_ActiveTrails.Count; ++i)//控制运动中的光束
        {
            var activeTrail = m_ActiveTrails[i];
            
            activeTrail.renderer.GetPositions(pos);
            activeTrail.remainingTime -= Time.deltaTime;

            pos[0] += activeTrail.direction * 50.0f * Time.deltaTime;
            pos[1] += activeTrail.direction * 50.0f * Time.deltaTime;
            
            m_ActiveTrails[i].renderer.SetPositions(pos);
            
            if (m_ActiveTrails[i].remainingTime <= 0.0f)
            {
                m_ActiveTrails[i].renderer.gameObject.SetActive(false);
                m_ActiveTrails.RemoveAt(i);
                i--;
            }
        }
    }

    void UpdateControllerState()//刷新控制器状态
    {
        m_Animator.SetFloat("speed", m_Owner.Speed);//速度
        m_Animator.SetBool("grounded", m_Owner.Grounded);//在地否
        
        var info = m_Animator.GetCurrentAnimatorStateInfo(0);//获取动画管理器状态

        WeaponState newState;
        if (info.shortNameHash == fireNameHash)//开火
            newState = WeaponState.Firing;
        else if (info.shortNameHash == reloadNameHash)//装弹
            newState = WeaponState.Reloading;
        else
            newState = WeaponState.Idle;

        if (newState != m_CurrentState)//自动装弹
        {
            var oldState = m_CurrentState;
            m_CurrentState = newState;
            
            if (oldState == WeaponState.Firing)
            {//we just finished firing, so check if we need to auto reload
                if(m_ClipContent == 0)
                    Reload();
            }
        }

        if (triggerDown)//碰触完成
        {
            if (triggerType == TriggerType.Manual)
            {
                if (!m_ShotDone)
                {
                    m_ShotDone = true;
                    Fire();
                }
            }
            else
                Fire();
        }
    }
    
    /// <summary>
    /// This will compute the corrected position of the muzzle flash in world space. Since the weapon camera use a
    /// different FOV than the main camera, using the muzzle spot to spawn thing rendered by the main camera will appear
    /// disconnected from the muzzle flash. So this convert the muzzle post from
    /// world -> view weapon -> clip weapon -> inverse clip main cam -> inverse view cam -> corrected world pos
    /// </summary>
    /// <returns></returns>
    public Vector3 GetCorrectedMuzzlePlace()//返回弹药位置
    {
        Vector3 position = EndPoint.position;

        position = Controller.Instance.WeaponCamera.WorldToScreenPoint(position);
        position = Controller.Instance.MainCamera.ScreenToWorldPoint(position);

        return position;
    }
}

public class AmmoTypeAttribute : PropertyAttribute
{
    
}

public abstract class AmmoDisplay : MonoBehaviour//抽象类
{
    public abstract void UpdateAmount(int current, int max);
}

#if UNITY_EDITOR

//没有调用,目前不知道具体作用,以后再说吧
[CustomPropertyDrawer(typeof(AmmoTypeAttribute))]//自定义特定类型的编辑器界面
public class AmmoTypeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)//重写界面
    {
        AmmoDatabase ammoDB = GameDatabase.Instance.ammoDatabase;

        if (ammoDB.entries == null || ammoDB.entries.Length == 0)
        {
            EditorGUI.HelpBox(position, "Please define at least 1 ammo type in the Game Database", MessageType.Error);
        }
        else
        {
            int currentID = property.intValue;
            int currentIdx = -1;

            //this is pretty ineffective, maybe find a way to cache that if prove to take too much time
            string[] names = new string[ammoDB.entries.Length];
            for (int i = 0; i < ammoDB.entries.Length; ++i)
            {
                names[i] = ammoDB.entries[i].name;
                if (ammoDB.entries[i].id == currentID)
                    currentIdx = i;
            }

            EditorGUI.BeginChangeCheck();
            int idx = EditorGUI.Popup(position, "Ammo Type", currentIdx, names);
            if (EditorGUI.EndChangeCheck())
            {
                property.intValue = ammoDB.entries[idx].id;
            }
        }
    }
}

[CustomEditor(typeof(Weapon))]//自定义编辑器界面
public class WeaponEditor : Editor
{ 
   SerializedProperty m_TriggerTypeProp;
   SerializedProperty m_WeaponTypeProp;
   SerializedProperty m_FireRateProp;
   SerializedProperty m_ReloadTimeProp;
   SerializedProperty m_ClipSizeProp;
   SerializedProperty m_DamageProp;
   SerializedProperty m_AmmoTypeProp;
   SerializedProperty m_ProjectilePrefabProp;
   SerializedProperty m_ProjectileLaunchForceProp; 
   SerializedProperty m_EndPointProp; 
   SerializedProperty m_AdvancedSettingsProp;
   SerializedProperty m_FireAnimationClipProp;
   SerializedProperty m_ReloadAnimationClipProp;
   SerializedProperty m_FireAudioClipProp;
   SerializedProperty m_ReloadAudioClipProp;
   SerializedProperty m_PrefabRayTrailProp;
   SerializedProperty m_AmmoDisplayProp;

   void OnEnable()
   {
       m_TriggerTypeProp = serializedObject.FindProperty("triggerType");
       m_WeaponTypeProp = serializedObject.FindProperty("weaponType");
       m_FireRateProp = serializedObject.FindProperty("fireRate");
       m_ReloadTimeProp = serializedObject.FindProperty("reloadTime");
       m_ClipSizeProp = serializedObject.FindProperty("clipSize");
       m_DamageProp = serializedObject.FindProperty("damage");
       m_AmmoTypeProp = serializedObject.FindProperty("ammoType");
       m_ProjectilePrefabProp = serializedObject.FindProperty("projectilePrefab");
       m_ProjectileLaunchForceProp = serializedObject.FindProperty("projectileLaunchForce");
       m_EndPointProp = serializedObject.FindProperty("EndPoint");
       m_AdvancedSettingsProp = serializedObject.FindProperty("advancedSettings");
       m_FireAnimationClipProp = serializedObject.FindProperty("FireAnimationClip");
       m_ReloadAnimationClipProp = serializedObject.FindProperty("ReloadAnimationClip");
       m_FireAudioClipProp = serializedObject.FindProperty("FireAudioClip");
       m_ReloadAudioClipProp = serializedObject.FindProperty("ReloadAudioClip");
       m_PrefabRayTrailProp = serializedObject.FindProperty("PrefabRayTrail");
       m_AmmoDisplayProp = serializedObject.FindProperty("AmmoDisplay");
   }

   public override void OnInspectorGUI()
    {
        serializedObject.Update();
        
        EditorGUILayout.PropertyField(m_TriggerTypeProp);
        EditorGUILayout.PropertyField(m_WeaponTypeProp);
        EditorGUILayout.PropertyField(m_FireRateProp);
        EditorGUILayout.PropertyField(m_ReloadTimeProp);
        EditorGUILayout.PropertyField(m_ClipSizeProp);
        EditorGUILayout.PropertyField(m_DamageProp);
        EditorGUILayout.PropertyField(m_AmmoTypeProp);

        if (m_WeaponTypeProp.intValue == (int)Weapon.WeaponType.Projectile)
        {
            EditorGUILayout.PropertyField(m_ProjectilePrefabProp);
            EditorGUILayout.PropertyField(m_ProjectileLaunchForceProp);
        }
        
        EditorGUILayout.PropertyField(m_EndPointProp); 
        EditorGUILayout.PropertyField(m_AdvancedSettingsProp, new GUIContent("Advance Settings"), true);
        EditorGUILayout.PropertyField(m_FireAnimationClipProp);
        EditorGUILayout.PropertyField(m_ReloadAnimationClipProp);
        EditorGUILayout.PropertyField(m_FireAudioClipProp);
        EditorGUILayout.PropertyField(m_ReloadAudioClipProp);

        if (m_WeaponTypeProp.intValue == (int)Weapon.WeaponType.Raycast)
        {
            EditorGUILayout.PropertyField(m_PrefabRayTrailProp);
        }

        EditorGUILayout.PropertyField(m_AmmoDisplayProp);

        serializedObject.ApplyModifiedProperties();
    }
}
#endif
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章