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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章