美文网首页
OpenXR开发实战项目之VR FPS射击

OpenXR开发实战项目之VR FPS射击

作者: TonyWan_AR | 来源:发表于2023-10-30 15:24 被阅读0次

    一、框架视图

    二、关键代码

    PlayerInputHandler

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class PlayerInputHandler : MonoBehaviour
        {
            [Tooltip("Sensitivity multiplier for moving the camera around")]
            public float LookSensitivity = 1f;
    
            [Tooltip("Additional sensitivity multiplier for WebGL")]
            public float WebglLookSensitivityMultiplier = 0.25f;
    
            [Tooltip("Limit to consider an input when using a trigger on a controller")]
            public float TriggerAxisThreshold = 0.4f;
    
            [Tooltip("Used to flip the vertical input axis")]
            public bool InvertYAxis = false;
    
            [Tooltip("Used to flip the horizontal input axis")]
            public bool InvertXAxis = false;
    
            GameFlowManager m_GameFlowManager;
            PlayerCharacterController m_PlayerCharacterController;
            bool m_FireInputWasHeld;
    
            void Start()
            {
                m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, PlayerInputHandler>(
                    m_PlayerCharacterController, this, gameObject);
                m_GameFlowManager = FindObjectOfType<GameFlowManager>();
                DebugUtility.HandleErrorIfNullFindObject<GameFlowManager, PlayerInputHandler>(m_GameFlowManager, this);
    
                Cursor.lockState = CursorLockMode.Locked;
                Cursor.visible = false;
            }
    
            void LateUpdate()
            {
                m_FireInputWasHeld = GetFireInputHeld();
            }
    
            public bool CanProcessInput()
            {
                return Cursor.lockState == CursorLockMode.Locked && !m_GameFlowManager.GameIsEnding;
            }
    
            public Vector3 GetMoveInput()
            {
                if (CanProcessInput())
                {
                    Vector3 move = new Vector3(Input.GetAxisRaw(GameConstants.k_AxisNameHorizontal), 0f,
                        Input.GetAxisRaw(GameConstants.k_AxisNameVertical));
    
                    // constrain move input to a maximum magnitude of 1, otherwise diagonal movement might exceed the max move speed defined
                    move = Vector3.ClampMagnitude(move, 1);
    
                    return move;
                }
    
                return Vector3.zero;
            }
    
            public float GetLookInputsHorizontal()
            {
                return GetMouseOrStickLookAxis(GameConstants.k_MouseAxisNameHorizontal,
                    GameConstants.k_AxisNameJoystickLookHorizontal);
            }
    
            public float GetLookInputsVertical()
            {
                return GetMouseOrStickLookAxis(GameConstants.k_MouseAxisNameVertical,
                    GameConstants.k_AxisNameJoystickLookVertical);
            }
    
            public bool GetJumpInputDown()
            {
                if (CanProcessInput())
                {
                    return Input.GetButtonDown(GameConstants.k_ButtonNameJump);
                }
    
                return false;
            }
    
            public bool GetJumpInputHeld()
            {
                if (CanProcessInput())
                {
                    return Input.GetButton(GameConstants.k_ButtonNameJump);
                }
    
                return false;
            }
    
            public bool GetFireInputDown()
            {
                return GetFireInputHeld() && !m_FireInputWasHeld;
            }
    
            public bool GetFireInputReleased()
            {
                return !GetFireInputHeld() && m_FireInputWasHeld;
            }
    
            public bool GetFireInputHeld()
            {
                if (CanProcessInput())
                {
                    bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadFire) != 0f;
                    if (isGamepad)
                    {
                        return Input.GetAxis(GameConstants.k_ButtonNameGamepadFire) >= TriggerAxisThreshold;
                    }
                    else
                    {
                        return Input.GetButton(GameConstants.k_ButtonNameFire);
                    }
                }
    
                return false;
            }
    
            public bool GetAimInputHeld()
            {
                if (CanProcessInput())
                {
                    bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadAim) != 0f;
                    bool i = isGamepad
                        ? (Input.GetAxis(GameConstants.k_ButtonNameGamepadAim) > 0f)
                        : Input.GetButton(GameConstants.k_ButtonNameAim);
                    return i;
                }
    
                return false;
            }
    
            public bool GetSprintInputHeld()
            {
                if (CanProcessInput())
                {
                    return Input.GetButton(GameConstants.k_ButtonNameSprint);
                }
    
                return false;
            }
    
            public bool GetCrouchInputDown()
            {
                if (CanProcessInput())
                {
                    return Input.GetButtonDown(GameConstants.k_ButtonNameCrouch);
                }
    
                return false;
            }
    
            public bool GetCrouchInputReleased()
            {
                if (CanProcessInput())
                {
                    return Input.GetButtonUp(GameConstants.k_ButtonNameCrouch);
                }
    
                return false;
            }
    
            public bool GetReloadButtonDown()
            {
                if (CanProcessInput())
                {
                    return Input.GetButtonDown(GameConstants.k_ButtonReload);
                }
    
                return false;
            }
    
            public int GetSwitchWeaponInput()
            {
                if (CanProcessInput())
                {
    
                    bool isGamepad = Input.GetAxis(GameConstants.k_ButtonNameGamepadSwitchWeapon) != 0f;
                    string axisName = isGamepad
                        ? GameConstants.k_ButtonNameGamepadSwitchWeapon
                        : GameConstants.k_ButtonNameSwitchWeapon;
    
                    if (Input.GetAxis(axisName) > 0f)
                        return -1;
                    else if (Input.GetAxis(axisName) < 0f)
                        return 1;
                    else if (Input.GetAxis(GameConstants.k_ButtonNameNextWeapon) > 0f)
                        return -1;
                    else if (Input.GetAxis(GameConstants.k_ButtonNameNextWeapon) < 0f)
                        return 1;
                }
    
                return 0;
            }
    
            public int GetSelectWeaponInput()
            {
                if (CanProcessInput())
                {
                    if (Input.GetKeyDown(KeyCode.Alpha1))
                        return 1;
                    else if (Input.GetKeyDown(KeyCode.Alpha2))
                        return 2;
                    else if (Input.GetKeyDown(KeyCode.Alpha3))
                        return 3;
                    else if (Input.GetKeyDown(KeyCode.Alpha4))
                        return 4;
                    else if (Input.GetKeyDown(KeyCode.Alpha5))
                        return 5;
                    else if (Input.GetKeyDown(KeyCode.Alpha6))
                        return 6;
                    else if (Input.GetKeyDown(KeyCode.Alpha7))
                        return 7;
                    else if (Input.GetKeyDown(KeyCode.Alpha8))
                        return 8;
                    else if (Input.GetKeyDown(KeyCode.Alpha9))
                        return 9;
                    else
                        return 0;
                }
    
                return 0;
            }
    
            float GetMouseOrStickLookAxis(string mouseInputName, string stickInputName)
            {
                if (CanProcessInput())
                {
                    // Check if this look input is coming from the mouse
                    bool isGamepad = Input.GetAxis(stickInputName) != 0f;
                    float i = isGamepad ? Input.GetAxis(stickInputName) : Input.GetAxisRaw(mouseInputName);
    
                    // handle inverting vertical input
                    if (InvertYAxis)
                        i *= -1f;
    
                    // apply sensitivity multiplier
                    i *= LookSensitivity;
    
                    if (isGamepad)
                    {
                        // since mouse input is already deltaTime-dependant, only scale input with frame time if it's coming from sticks
                        i *= Time.deltaTime;
                    }
                    else
                    {
                        // reduce mouse input amount to be equivalent to stick movement
                        i *= 0.01f;
    #if UNITY_WEBGL
                        // Mouse tends to be even more sensitive in WebGL due to mouse acceleration, so reduce it even more
                        i *= WebglLookSensitivityMultiplier;
    #endif
                    }
    
                    return i;
                }
    
                return 0f;
            }
        }
    }
    

    PlayerWeaponsManager

    using System.Collections.Generic;
    using Unity.FPS.Game;
    using UnityEngine;
    using UnityEngine.Events;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(PlayerInputHandler))]
        public class PlayerWeaponsManager : MonoBehaviour
        {
            public enum WeaponSwitchState
            {
                Up,
                Down,
                PutDownPrevious,
                PutUpNew,
            }
    
            [Tooltip("List of weapon the player will start with")]
            public List<WeaponController> StartingWeapons = new List<WeaponController>();
    
            [Header("References")] [Tooltip("Secondary camera used to avoid seeing weapon go throw geometries")]
            public Camera WeaponCamera;
    
            [Tooltip("Parent transform where all weapon will be added in the hierarchy")]
            public Transform WeaponParentSocket;
    
            [Tooltip("Position for weapons when active but not actively aiming")]
            public Transform DefaultWeaponPosition;
    
            [Tooltip("Position for weapons when aiming")]
            public Transform AimingWeaponPosition;
    
            [Tooltip("Position for innactive weapons")]
            public Transform DownWeaponPosition;
    
            [Header("Weapon Bob")]
            [Tooltip("Frequency at which the weapon will move around in the screen when the player is in movement")]
            public float BobFrequency = 10f;
    
            [Tooltip("How fast the weapon bob is applied, the bigger value the fastest")]
            public float BobSharpness = 10f;
    
            [Tooltip("Distance the weapon bobs when not aiming")]
            public float DefaultBobAmount = 0.05f;
    
            [Tooltip("Distance the weapon bobs when aiming")]
            public float AimingBobAmount = 0.02f;
    
            [Header("Weapon Recoil")]
            [Tooltip("This will affect how fast the recoil moves the weapon, the bigger the value, the fastest")]
            public float RecoilSharpness = 50f;
    
            [Tooltip("Maximum distance the recoil can affect the weapon")]
            public float MaxRecoilDistance = 0.5f;
    
            [Tooltip("How fast the weapon goes back to it's original position after the recoil is finished")]
            public float RecoilRestitutionSharpness = 10f;
    
            [Header("Misc")] [Tooltip("Speed at which the aiming animatoin is played")]
            public float AimingAnimationSpeed = 10f;
    
            [Tooltip("Field of view when not aiming")]
            public float DefaultFov = 60f;
    
            [Tooltip("Portion of the regular FOV to apply to the weapon camera")]
            public float WeaponFovMultiplier = 1f;
    
            [Tooltip("Delay before switching weapon a second time, to avoid recieving multiple inputs from mouse wheel")]
            public float WeaponSwitchDelay = 1f;
    
            [Tooltip("Layer to set FPS weapon gameObjects to")]
            public LayerMask FpsWeaponLayer;
    
            public bool IsAiming { get; private set; }
            public bool IsPointingAtEnemy { get; private set; }
            public int ActiveWeaponIndex { get; private set; }
    
            public UnityAction<WeaponController> OnSwitchedToWeapon;
            public UnityAction<WeaponController, int> OnAddedWeapon;
            public UnityAction<WeaponController, int> OnRemovedWeapon;
    
            WeaponController[] m_WeaponSlots = new WeaponController[9]; // 9 available weapon slots
            PlayerInputHandler m_InputHandler;
            PlayerCharacterController m_PlayerCharacterController;
            float m_WeaponBobFactor;
            Vector3 m_LastCharacterPosition;
            Vector3 m_WeaponMainLocalPosition;
            Vector3 m_WeaponBobLocalPosition;
            Vector3 m_WeaponRecoilLocalPosition;
            Vector3 m_AccumulatedRecoil;
            float m_TimeStartedWeaponSwitch;
            WeaponSwitchState m_WeaponSwitchState;
            int m_WeaponSwitchNewWeaponIndex;
    
            void Start()
            {
                ActiveWeaponIndex = -1;
                m_WeaponSwitchState = WeaponSwitchState.Down;
    
                m_InputHandler = GetComponent<PlayerInputHandler>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, PlayerWeaponsManager>(m_InputHandler, this,
                    gameObject);
    
                m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, PlayerWeaponsManager>(
                    m_PlayerCharacterController, this, gameObject);
    
                SetFov(DefaultFov);
    
                OnSwitchedToWeapon += OnWeaponSwitched;
    
                // Add starting weapons
                foreach (var weapon in StartingWeapons)
                {
                    AddWeapon(weapon);
                }
    
                SwitchWeapon(true);
            }
    
            void Update()
            {
                // shoot handling
                WeaponController activeWeapon = GetActiveWeapon();
    
                if (activeWeapon != null && activeWeapon.IsReloading)
                    return;
    
                if (activeWeapon != null && m_WeaponSwitchState == WeaponSwitchState.Up)
                {
                    if (!activeWeapon.AutomaticReload && m_InputHandler.GetReloadButtonDown() && activeWeapon.CurrentAmmoRatio < 1.0f)
                    {
                        IsAiming = false;
                        activeWeapon.StartReloadAnimation();
                        return;
                    }
                    // handle aiming down sights
                    IsAiming = m_InputHandler.GetAimInputHeld();
    
                    // handle shooting
                    bool hasFired = activeWeapon.HandleShootInputs(
                        m_InputHandler.GetFireInputDown(),
                        m_InputHandler.GetFireInputHeld(),
                        m_InputHandler.GetFireInputReleased());
    
                    // Handle accumulating recoil
                    if (hasFired)
                    {
                        m_AccumulatedRecoil += Vector3.back * activeWeapon.RecoilForce;
                        m_AccumulatedRecoil = Vector3.ClampMagnitude(m_AccumulatedRecoil, MaxRecoilDistance);
                    }
                }
    
                // weapon switch handling
                if (!IsAiming &&
                    (activeWeapon == null || !activeWeapon.IsCharging) &&
                    (m_WeaponSwitchState == WeaponSwitchState.Up || m_WeaponSwitchState == WeaponSwitchState.Down))
                {
                    int switchWeaponInput = m_InputHandler.GetSwitchWeaponInput();
                    if (switchWeaponInput != 0)
                    {
                        bool switchUp = switchWeaponInput > 0;
                        SwitchWeapon(switchUp);
                    }
                    else
                    {
                        switchWeaponInput = m_InputHandler.GetSelectWeaponInput();
                        if (switchWeaponInput != 0)
                        {
                            if (GetWeaponAtSlotIndex(switchWeaponInput - 1) != null)
                                SwitchToWeaponIndex(switchWeaponInput - 1);
                        }
                    }
                }
    
                // Pointing at enemy handling
                IsPointingAtEnemy = false;
                if (activeWeapon)
                {
                    if (Physics.Raycast(WeaponCamera.transform.position, WeaponCamera.transform.forward, out RaycastHit hit,
                        1000, -1, QueryTriggerInteraction.Ignore))
                    {
                        if (hit.collider.GetComponentInParent<Health>() != null)
                        {
                            IsPointingAtEnemy = true;
                        }
                    }
                }
            }
    
    
            // Update various animated features in LateUpdate because it needs to override the animated arm position
            void LateUpdate()
            {
                UpdateWeaponAiming();
                UpdateWeaponBob();
                UpdateWeaponRecoil();
                UpdateWeaponSwitching();
    
                // Set final weapon socket position based on all the combined animation influences
                WeaponParentSocket.localPosition =
                    m_WeaponMainLocalPosition + m_WeaponBobLocalPosition + m_WeaponRecoilLocalPosition;
            }
    
            // Sets the FOV of the main camera and the weapon camera simultaneously
            public void SetFov(float fov)
            {
                m_PlayerCharacterController.PlayerCamera.fieldOfView = fov;
                WeaponCamera.fieldOfView = fov * WeaponFovMultiplier;
            }
    
            // Iterate on all weapon slots to find the next valid weapon to switch to
            public void SwitchWeapon(bool ascendingOrder)
            {
                int newWeaponIndex = -1;
                int closestSlotDistance = m_WeaponSlots.Length;
                for (int i = 0; i < m_WeaponSlots.Length; i++)
                {
                    // If the weapon at this slot is valid, calculate its "distance" from the active slot index (either in ascending or descending order)
                    // and select it if it's the closest distance yet
                    if (i != ActiveWeaponIndex && GetWeaponAtSlotIndex(i) != null)
                    {
                        int distanceToActiveIndex = GetDistanceBetweenWeaponSlots(ActiveWeaponIndex, i, ascendingOrder);
    
                        if (distanceToActiveIndex < closestSlotDistance)
                        {
                            closestSlotDistance = distanceToActiveIndex;
                            newWeaponIndex = i;
                        }
                    }
                }
    
                // Handle switching to the new weapon index
                SwitchToWeaponIndex(newWeaponIndex);
            }
    
            // Switches to the given weapon index in weapon slots if the new index is a valid weapon that is different from our current one
            public void SwitchToWeaponIndex(int newWeaponIndex, bool force = false)
            {
                if (force || (newWeaponIndex != ActiveWeaponIndex && newWeaponIndex >= 0))
                {
                    // Store data related to weapon switching animation
                    m_WeaponSwitchNewWeaponIndex = newWeaponIndex;
                    m_TimeStartedWeaponSwitch = Time.time;
    
                    // Handle case of switching to a valid weapon for the first time (simply put it up without putting anything down first)
                    if (GetActiveWeapon() == null)
                    {
                        m_WeaponMainLocalPosition = DownWeaponPosition.localPosition;
                        m_WeaponSwitchState = WeaponSwitchState.PutUpNew;
                        ActiveWeaponIndex = m_WeaponSwitchNewWeaponIndex;
    
                        WeaponController newWeapon = GetWeaponAtSlotIndex(m_WeaponSwitchNewWeaponIndex);
                        if (OnSwitchedToWeapon != null)
                        {
                            OnSwitchedToWeapon.Invoke(newWeapon);
                        }
                    }
                    // otherwise, remember we are putting down our current weapon for switching to the next one
                    else
                    {
                        m_WeaponSwitchState = WeaponSwitchState.PutDownPrevious;
                    }
                }
            }
    
            public WeaponController HasWeapon(WeaponController weaponPrefab)
            {
                // Checks if we already have a weapon coming from the specified prefab
                for (var index = 0; index < m_WeaponSlots.Length; index++)
                {
                    var w = m_WeaponSlots[index];
                    if (w != null && w.SourcePrefab == weaponPrefab.gameObject)
                    {
                        return w;
                    }
                }
    
                return null;
            }
    
            // Updates weapon position and camera FoV for the aiming transition
            void UpdateWeaponAiming()
            {
                if (m_WeaponSwitchState == WeaponSwitchState.Up)
                {
                    WeaponController activeWeapon = GetActiveWeapon();
                    if (IsAiming && activeWeapon)
                    {
                        m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition,
                            AimingWeaponPosition.localPosition + activeWeapon.AimOffset,
                            AimingAnimationSpeed * Time.deltaTime);
                        SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView,
                            activeWeapon.AimZoomRatio * DefaultFov, AimingAnimationSpeed * Time.deltaTime));
                    }
                    else
                    {
                        m_WeaponMainLocalPosition = Vector3.Lerp(m_WeaponMainLocalPosition,
                            DefaultWeaponPosition.localPosition, AimingAnimationSpeed * Time.deltaTime);
                        SetFov(Mathf.Lerp(m_PlayerCharacterController.PlayerCamera.fieldOfView, DefaultFov,
                            AimingAnimationSpeed * Time.deltaTime));
                    }
                }
            }
    
            // Updates the weapon bob animation based on character speed
            void UpdateWeaponBob()
            {
                if (Time.deltaTime > 0f)
                {
                    Vector3 playerCharacterVelocity =
                        (m_PlayerCharacterController.transform.position - m_LastCharacterPosition) / Time.deltaTime;
    
                    // calculate a smoothed weapon bob amount based on how close to our max grounded movement velocity we are
                    float characterMovementFactor = 0f;
                    if (m_PlayerCharacterController.IsGrounded)
                    {
                        characterMovementFactor =
                            Mathf.Clamp01(playerCharacterVelocity.magnitude /
                                          (m_PlayerCharacterController.MaxSpeedOnGround *
                                           m_PlayerCharacterController.SprintSpeedModifier));
                    }
    
                    m_WeaponBobFactor =
                        Mathf.Lerp(m_WeaponBobFactor, characterMovementFactor, BobSharpness * Time.deltaTime);
    
                    // Calculate vertical and horizontal weapon bob values based on a sine function
                    float bobAmount = IsAiming ? AimingBobAmount : DefaultBobAmount;
                    float frequency = BobFrequency;
                    float hBobValue = Mathf.Sin(Time.time * frequency) * bobAmount * m_WeaponBobFactor;
                    float vBobValue = ((Mathf.Sin(Time.time * frequency * 2f) * 0.5f) + 0.5f) * bobAmount *
                                      m_WeaponBobFactor;
    
                    // Apply weapon bob
                    m_WeaponBobLocalPosition.x = hBobValue;
                    m_WeaponBobLocalPosition.y = Mathf.Abs(vBobValue);
    
                    m_LastCharacterPosition = m_PlayerCharacterController.transform.position;
                }
            }
    
            // Updates the weapon recoil animation
            void UpdateWeaponRecoil()
            {
                // if the accumulated recoil is further away from the current position, make the current position move towards the recoil target
                if (m_WeaponRecoilLocalPosition.z >= m_AccumulatedRecoil.z * 0.99f)
                {
                    m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, m_AccumulatedRecoil,
                        RecoilSharpness * Time.deltaTime);
                }
                // otherwise, move recoil position to make it recover towards its resting pose
                else
                {
                    m_WeaponRecoilLocalPosition = Vector3.Lerp(m_WeaponRecoilLocalPosition, Vector3.zero,
                        RecoilRestitutionSharpness * Time.deltaTime);
                    m_AccumulatedRecoil = m_WeaponRecoilLocalPosition;
                }
            }
    
            // Updates the animated transition of switching weapons
            void UpdateWeaponSwitching()
            {
                // Calculate the time ratio (0 to 1) since weapon switch was triggered
                float switchingTimeFactor = 0f;
                if (WeaponSwitchDelay == 0f)
                {
                    switchingTimeFactor = 1f;
                }
                else
                {
                    switchingTimeFactor = Mathf.Clamp01((Time.time - m_TimeStartedWeaponSwitch) / WeaponSwitchDelay);
                }
    
                // Handle transiting to new switch state
                if (switchingTimeFactor >= 1f)
                {
                    if (m_WeaponSwitchState == WeaponSwitchState.PutDownPrevious)
                    {
                        // Deactivate old weapon
                        WeaponController oldWeapon = GetWeaponAtSlotIndex(ActiveWeaponIndex);
                        if (oldWeapon != null)
                        {
                            oldWeapon.ShowWeapon(false);
                        }
    
                        ActiveWeaponIndex = m_WeaponSwitchNewWeaponIndex;
                        switchingTimeFactor = 0f;
    
                        // Activate new weapon
                        WeaponController newWeapon = GetWeaponAtSlotIndex(ActiveWeaponIndex);
                        if (OnSwitchedToWeapon != null)
                        {
                            OnSwitchedToWeapon.Invoke(newWeapon);
                        }
    
                        if (newWeapon)
                        {
                            m_TimeStartedWeaponSwitch = Time.time;
                            m_WeaponSwitchState = WeaponSwitchState.PutUpNew;
                        }
                        else
                        {
                            // if new weapon is null, don't follow through with putting weapon back up
                            m_WeaponSwitchState = WeaponSwitchState.Down;
                        }
                    }
                    else if (m_WeaponSwitchState == WeaponSwitchState.PutUpNew)
                    {
                        m_WeaponSwitchState = WeaponSwitchState.Up;
                    }
                }
    
                // Handle moving the weapon socket position for the animated weapon switching
                if (m_WeaponSwitchState == WeaponSwitchState.PutDownPrevious)
                {
                    m_WeaponMainLocalPosition = Vector3.Lerp(DefaultWeaponPosition.localPosition,
                        DownWeaponPosition.localPosition, switchingTimeFactor);
                }
                else if (m_WeaponSwitchState == WeaponSwitchState.PutUpNew)
                {
                    m_WeaponMainLocalPosition = Vector3.Lerp(DownWeaponPosition.localPosition,
                        DefaultWeaponPosition.localPosition, switchingTimeFactor);
                }
            }
    
            // Adds a weapon to our inventory
            public bool AddWeapon(WeaponController weaponPrefab)
            {
                // if we already hold this weapon type (a weapon coming from the same source prefab), don't add the weapon
                if (HasWeapon(weaponPrefab) != null)
                {
                    return false;
                }
    
                // search our weapon slots for the first free one, assign the weapon to it, and return true if we found one. Return false otherwise
                for (int i = 0; i < m_WeaponSlots.Length; i++)
                {
                    // only add the weapon if the slot is free
                    if (m_WeaponSlots[i] == null)
                    {
                        // spawn the weapon prefab as child of the weapon socket
                        WeaponController weaponInstance = Instantiate(weaponPrefab, WeaponParentSocket);
                        weaponInstance.transform.localPosition = Vector3.zero;
                        weaponInstance.transform.localRotation = Quaternion.identity;
    
                        // Set owner to this gameObject so the weapon can alter projectile/damage logic accordingly
                        weaponInstance.Owner = gameObject;
                        weaponInstance.SourcePrefab = weaponPrefab.gameObject;
                        weaponInstance.ShowWeapon(false);
    
                        // Assign the first person layer to the weapon
                        int layerIndex =
                            Mathf.RoundToInt(Mathf.Log(FpsWeaponLayer.value,
                                2)); // This function converts a layermask to a layer index
                        foreach (Transform t in weaponInstance.gameObject.GetComponentsInChildren<Transform>(true))
                        {
                            t.gameObject.layer = layerIndex;
                        }
    
                        m_WeaponSlots[i] = weaponInstance;
    
                        if (OnAddedWeapon != null)
                        {
                            OnAddedWeapon.Invoke(weaponInstance, i);
                        }
    
                        return true;
                    }
                }
    
                // Handle auto-switching to weapon if no weapons currently
                if (GetActiveWeapon() == null)
                {
                    SwitchWeapon(true);
                }
    
                return false;
            }
    
            public bool RemoveWeapon(WeaponController weaponInstance)
            {
                // Look through our slots for that weapon
                for (int i = 0; i < m_WeaponSlots.Length; i++)
                {
                    // when weapon found, remove it
                    if (m_WeaponSlots[i] == weaponInstance)
                    {
                        m_WeaponSlots[i] = null;
    
                        if (OnRemovedWeapon != null)
                        {
                            OnRemovedWeapon.Invoke(weaponInstance, i);
                        }
    
                        Destroy(weaponInstance.gameObject);
    
                        // Handle case of removing active weapon (switch to next weapon)
                        if (i == ActiveWeaponIndex)
                        {
                            SwitchWeapon(true);
                        }
    
                        return true;
                    }
                }
    
                return false;
            }
    
            public WeaponController GetActiveWeapon()
            {
                return GetWeaponAtSlotIndex(ActiveWeaponIndex);
            }
    
            public WeaponController GetWeaponAtSlotIndex(int index)
            {
                // find the active weapon in our weapon slots based on our active weapon index
                if (index >= 0 &&
                    index < m_WeaponSlots.Length)
                {
                    return m_WeaponSlots[index];
                }
    
                // if we didn't find a valid active weapon in our weapon slots, return null
                return null;
            }
    
            // Calculates the "distance" between two weapon slot indexes
            // For example: if we had 5 weapon slots, the distance between slots #2 and #4 would be 2 in ascending order, and 3 in descending order
            int GetDistanceBetweenWeaponSlots(int fromSlotIndex, int toSlotIndex, bool ascendingOrder)
            {
                int distanceBetweenSlots = 0;
    
                if (ascendingOrder)
                {
                    distanceBetweenSlots = toSlotIndex - fromSlotIndex;
                }
                else
                {
                    distanceBetweenSlots = -1 * (toSlotIndex - fromSlotIndex);
                }
    
                if (distanceBetweenSlots < 0)
                {
                    distanceBetweenSlots = m_WeaponSlots.Length + distanceBetweenSlots;
                }
    
                return distanceBetweenSlots;
            }
    
            void OnWeaponSwitched(WeaponController newWeapon)
            {
                if (newWeapon != null)
                {
                    newWeapon.ShowWeapon(true);
                }
            }
        }
    }
    

    ObjectiveKillEnemies

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class ObjectiveKillEnemies : Objective
        {
            [Tooltip("Chose whether you need to kill every enemies or only a minimum amount")]
            public bool MustKillAllEnemies = true;
    
            [Tooltip("If MustKillAllEnemies is false, this is the amount of enemy kills required")]
            public int KillsToCompleteObjective = 5;
    
            [Tooltip("Start sending notification about remaining enemies when this amount of enemies is left")]
            public int NotificationEnemiesRemainingThreshold = 3;
    
            int m_KillTotal;
    
            protected override void Start()
            {
                base.Start();
    
                EventManager.AddListener<EnemyKillEvent>(OnEnemyKilled);
    
                // set a title and description specific for this type of objective, if it hasn't one
                if (string.IsNullOrEmpty(Title))
                    Title = "Eliminate " + (MustKillAllEnemies ? "all the" : KillsToCompleteObjective.ToString()) +
                            " enemies";
    
                if (string.IsNullOrEmpty(Description))
                    Description = GetUpdatedCounterAmount();
            }
    
            void OnEnemyKilled(EnemyKillEvent evt)
            {
                if (IsCompleted)
                    return;
    
                m_KillTotal++;
    
                if (MustKillAllEnemies)
                    KillsToCompleteObjective = evt.RemainingEnemyCount + m_KillTotal;
    
                int targetRemaining = MustKillAllEnemies ? evt.RemainingEnemyCount : KillsToCompleteObjective - m_KillTotal;
    
                // update the objective text according to how many enemies remain to kill
                if (targetRemaining == 0)
                {
                    CompleteObjective(string.Empty, GetUpdatedCounterAmount(), "Objective complete : " + Title);
                }
                else if (targetRemaining == 1)
                {
                    string notificationText = NotificationEnemiesRemainingThreshold >= targetRemaining
                        ? "One enemy left"
                        : string.Empty;
                    UpdateObjective(string.Empty, GetUpdatedCounterAmount(), notificationText);
                }
                else
                {
                    // create a notification text if needed, if it stays empty, the notification will not be created
                    string notificationText = NotificationEnemiesRemainingThreshold >= targetRemaining
                        ? targetRemaining + " enemies to kill left"
                        : string.Empty;
    
                    UpdateObjective(string.Empty, GetUpdatedCounterAmount(), notificationText);
                }
            }
    
            string GetUpdatedCounterAmount()
            {
                return m_KillTotal + " / " + KillsToCompleteObjective;
            }
    
            void OnDestroy()
            {
                EventManager.RemoveListener<EnemyKillEvent>(OnEnemyKilled);
            }
        }
    }
    

    ObjectivePickupItem

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class ObjectivePickupItem : Objective
        {
            [Tooltip("Item to pickup to complete the objective")]
            public GameObject ItemToPickup;
    
            protected override void Start()
            {
                base.Start();
    
                EventManager.AddListener<PickupEvent>(OnPickupEvent);
            }
    
            void OnPickupEvent(PickupEvent evt)
            {
                if (IsCompleted || ItemToPickup != evt.Pickup)
                    return;
    
                // this will trigger the objective completion
                // it works even if the player can't pickup the item (i.e. objective pickup healthpack while at full heath)
                CompleteObjective(string.Empty, string.Empty, "Objective complete : " + Title);
    
                if (gameObject)
                {
                    Destroy(gameObject);
                }
            }
    
            void OnDestroy()
            {
                EventManager.RemoveListener<PickupEvent>(OnPickupEvent);
            }
        }
    }
    

    ObjectiveReachPoint

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(Collider))]
        public class ObjectiveReachPoint : Objective
        {
            [Tooltip("Visible transform that will be destroyed once the objective is completed")]
            public Transform DestroyRoot;
    
            void Awake()
            {
                if (DestroyRoot == null)
                    DestroyRoot = transform;
            }
    
            void OnTriggerEnter(Collider other)
            {
                if (IsCompleted)
                    return;
    
                var player = other.GetComponent<PlayerCharacterController>();
                // test if the other collider contains a PlayerCharacterController, then complete
                if (player != null)
                {
                    CompleteObjective(string.Empty, string.Empty, "Objective complete : " + Title);
    
                    // destroy the transform, will remove the compass marker if it has one
                    Destroy(DestroyRoot.gameObject);
                }
            }
        }
    }
    

    AmmoPickup

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class AmmoPickup : Pickup
        {
            [Tooltip("Weapon those bullets are for")]
            public WeaponController Weapon;
    
            [Tooltip("Number of bullets the player gets")]
            public int BulletCount = 30;
    
            protected override void OnPicked(PlayerCharacterController byPlayer)
            {
                PlayerWeaponsManager playerWeaponsManager = byPlayer.GetComponent<PlayerWeaponsManager>();
                if (playerWeaponsManager)
                {
                    WeaponController weapon = playerWeaponsManager.HasWeapon(Weapon);
                    if (weapon != null)
                    {
                        weapon.AddCarriablePhysicalBullets(BulletCount);
    
                        AmmoPickupEvent evt = Events.AmmoPickupEvent;
                        evt.Weapon = weapon;
                        EventManager.Broadcast(evt);
    
                        PlayPickupFeedback();
                        Destroy(gameObject);
                    }
                }
            }
        }
    }
    
    

    ChargedProjectileEffectsHandler

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class ChargedProjectileEffectsHandler : MonoBehaviour
        {
            [Tooltip("Object that will be affected by charging scale & color changes")]
            public GameObject ChargingObject;
    
            [Tooltip("Scale of the charged object based on charge")]
            public MinMaxVector3 Scale;
    
            [Tooltip("Color of the charged object based on charge")]
            public MinMaxColor Color;
    
            MeshRenderer[] m_AffectedRenderers;
            ProjectileBase m_ProjectileBase;
    
            void OnEnable()
            {
                m_ProjectileBase = GetComponent<ProjectileBase>();
                DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ChargedProjectileEffectsHandler>(
                    m_ProjectileBase, this, gameObject);
    
                m_ProjectileBase.OnShoot += OnShoot;
    
                m_AffectedRenderers = ChargingObject.GetComponentsInChildren<MeshRenderer>();
                foreach (var ren in m_AffectedRenderers)
                {
                    ren.sharedMaterial = Instantiate(ren.sharedMaterial);
                }
            }
    
            void OnShoot()
            {
                ChargingObject.transform.localScale = Scale.GetValueFromRatio(m_ProjectileBase.InitialCharge);
    
                foreach (var ren in m_AffectedRenderers)
                {
                    ren.sharedMaterial.SetColor("_Color", Color.GetValueFromRatio(m_ProjectileBase.InitialCharge));
                }
            }
        }
    }
    

    ChargedWeaponEffectsHandler

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(AudioSource))]
        public class ChargedWeaponEffectsHandler : MonoBehaviour
        {
            [Header("Visual")] [Tooltip("Object that will be affected by charging scale & color changes")]
            public GameObject ChargingObject;
    
            [Tooltip("The spinning frame")] public GameObject SpinningFrame;
    
            [Tooltip("Scale of the charged object based on charge")]
            public MinMaxVector3 Scale;
    
            [Header("Particles")] [Tooltip("Particles to create when charging")]
            public GameObject DiskOrbitParticlePrefab;
    
            [Tooltip("Local position offset of the charge particles (relative to this transform)")]
            public Vector3 Offset;
    
            [Tooltip("Parent transform for the particles (Optional)")]
            public Transform ParentTransform;
    
            [Tooltip("Orbital velocity of the charge particles based on charge")]
            public MinMaxFloat OrbitY;
    
            [Tooltip("Radius of the charge particles based on charge")]
            public MinMaxVector3 Radius;
    
            [Tooltip("Idle spinning speed of the frame based on charge")]
            public MinMaxFloat SpinningSpeed;
    
            [Header("Sound")] [Tooltip("Audio clip for charge SFX")]
            public AudioClip ChargeSound;
    
            [Tooltip("Sound played in loop after the change is full for this weapon")]
            public AudioClip LoopChargeWeaponSfx;
    
            [Tooltip("Duration of the cross fade between the charge and the loop sound")]
            public float FadeLoopDuration = 0.5f;
    
            [Tooltip(
                "If true, the ChargeSound will be ignored and the pitch on the LoopSound will be procedural, based on the charge amount")]
            public bool UseProceduralPitchOnLoopSfx;
    
            [Range(1.0f, 5.0f), Tooltip("Maximum procedural Pitch value")]
            public float MaxProceduralPitchValue = 2.0f;
    
            public GameObject ParticleInstance { get; set; }
    
            ParticleSystem m_DiskOrbitParticle;
            WeaponController m_WeaponController;
            ParticleSystem.VelocityOverLifetimeModule m_VelocityOverTimeModule;
    
            AudioSource m_AudioSource;
            AudioSource m_AudioSourceLoop;
    
            float m_LastChargeTriggerTimestamp;
            float m_ChargeRatio;
            float m_EndchargeTime;
    
            void Awake()
            {
                m_LastChargeTriggerTimestamp = 0.0f;
    
                // The charge effect needs it's own AudioSources, since it will play on top of the other gun sounds
                m_AudioSource = gameObject.AddComponent<AudioSource>();
                m_AudioSource.clip = ChargeSound;
                m_AudioSource.playOnAwake = false;
                m_AudioSource.outputAudioMixerGroup =
                    AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponChargeBuildup);
    
                // create a second audio source, to play the sound with a delay
                m_AudioSourceLoop = gameObject.AddComponent<AudioSource>();
                m_AudioSourceLoop.clip = LoopChargeWeaponSfx;
                m_AudioSourceLoop.playOnAwake = false;
                m_AudioSourceLoop.loop = true;
                m_AudioSourceLoop.outputAudioMixerGroup =
                    AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponChargeLoop);
            }
    
            void SpawnParticleSystem()
            {
                ParticleInstance = Instantiate(DiskOrbitParticlePrefab,
                    ParentTransform != null ? ParentTransform : transform);
                ParticleInstance.transform.localPosition += Offset;
    
                FindReferences();
            }
    
            public void FindReferences()
            {
                m_DiskOrbitParticle = ParticleInstance.GetComponent<ParticleSystem>();
                DebugUtility.HandleErrorIfNullGetComponent<ParticleSystem, ChargedWeaponEffectsHandler>(m_DiskOrbitParticle,
                    this, ParticleInstance.gameObject);
    
                m_WeaponController = GetComponent<WeaponController>();
                DebugUtility.HandleErrorIfNullGetComponent<WeaponController, ChargedWeaponEffectsHandler>(
                    m_WeaponController, this, gameObject);
    
                m_VelocityOverTimeModule = m_DiskOrbitParticle.velocityOverLifetime;
            }
    
            void Update()
            {
                if (ParticleInstance == null)
                    SpawnParticleSystem();
    
                m_DiskOrbitParticle.gameObject.SetActive(m_WeaponController.IsWeaponActive);
                m_ChargeRatio = m_WeaponController.CurrentCharge;
    
                ChargingObject.transform.localScale = Scale.GetValueFromRatio(m_ChargeRatio);
                if (SpinningFrame != null)
                {
                    SpinningFrame.transform.localRotation *= Quaternion.Euler(0,
                        SpinningSpeed.GetValueFromRatio(m_ChargeRatio) * Time.deltaTime, 0);
                }
    
                m_VelocityOverTimeModule.orbitalY = OrbitY.GetValueFromRatio(m_ChargeRatio);
                m_DiskOrbitParticle.transform.localScale = Radius.GetValueFromRatio(m_ChargeRatio * 1.1f);
    
                // update sound's volume and pitch 
                if (m_ChargeRatio > 0)
                {
                    if (!m_AudioSourceLoop.isPlaying &&
                        m_WeaponController.LastChargeTriggerTimestamp > m_LastChargeTriggerTimestamp)
                    {
                        m_LastChargeTriggerTimestamp = m_WeaponController.LastChargeTriggerTimestamp;
                        if (!UseProceduralPitchOnLoopSfx)
                        {
                            m_EndchargeTime = Time.time + ChargeSound.length;
                            m_AudioSource.Play();
                        }
    
                        m_AudioSourceLoop.Play();
                    }
    
                    if (!UseProceduralPitchOnLoopSfx)
                    {
                        float volumeRatio =
                            Mathf.Clamp01((m_EndchargeTime - Time.time - FadeLoopDuration) / FadeLoopDuration);
                        m_AudioSource.volume = volumeRatio;
                        m_AudioSourceLoop.volume = 1 - volumeRatio;
                    }
                    else
                    {
                        m_AudioSourceLoop.pitch = Mathf.Lerp(1.0f, MaxProceduralPitchValue, m_ChargeRatio);
                    }
                }
                else
                {
                    m_AudioSource.Stop();
                    m_AudioSourceLoop.Stop();
                }
            }
        }
    }
    

    HealthPickup

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class HealthPickup : Pickup
        {
            [Header("Parameters")] [Tooltip("Amount of health to heal on pickup")]
            public float HealAmount;
    
            protected override void OnPicked(PlayerCharacterController player)
            {
                Health playerHealth = player.GetComponent<Health>();
                if (playerHealth && playerHealth.CanPickup())
                {
                    playerHealth.Heal(HealAmount);
                    PlayPickupFeedback();
                    Destroy(gameObject);
                }
            }
        }
    }
    

    Jetpack

    using Unity.FPS.Game;
    using UnityEngine;
    using UnityEngine.Events;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(AudioSource))]
        public class Jetpack : MonoBehaviour
        {
            [Header("References")] [Tooltip("Audio source for jetpack sfx")]
            public AudioSource AudioSource;
    
            [Tooltip("Particles for jetpack vfx")] public ParticleSystem[] JetpackVfx;
    
            [Header("Parameters")] [Tooltip("Whether the jetpack is unlocked at the begining or not")]
            public bool IsJetpackUnlockedAtStart = false;
    
            [Tooltip("The strength with which the jetpack pushes the player up")]
            public float JetpackAcceleration = 7f;
    
            [Range(0f, 1f)]
            [Tooltip(
                "This will affect how much using the jetpack will cancel the gravity value, to start going up faster. 0 is not at all, 1 is instant")]
            public float JetpackDownwardVelocityCancelingFactor = 1f;
    
            [Header("Durations")] [Tooltip("Time it takes to consume all the jetpack fuel")]
            public float ConsumeDuration = 1.5f;
    
            [Tooltip("Time it takes to completely refill the jetpack while on the ground")]
            public float RefillDurationGrounded = 2f;
    
            [Tooltip("Time it takes to completely refill the jetpack while in the air")]
            public float RefillDurationInTheAir = 5f;
    
            [Tooltip("Delay after last use before starting to refill")]
            public float RefillDelay = 1f;
    
            [Header("Audio")] [Tooltip("Sound played when using the jetpack")]
            public AudioClip JetpackSfx;
    
            bool m_CanUseJetpack;
            PlayerCharacterController m_PlayerCharacterController;
            PlayerInputHandler m_InputHandler;
            float m_LastTimeOfUse;
    
            // stored ratio for jetpack resource (1 is full, 0 is empty)
            public float CurrentFillRatio { get; private set; }
            public bool IsJetpackUnlocked { get; private set; }
    
            public bool IsPlayergrounded() => m_PlayerCharacterController.IsGrounded;
    
            public UnityAction<bool> OnUnlockJetpack;
    
            void Start()
            {
                IsJetpackUnlocked = IsJetpackUnlockedAtStart;
    
                m_PlayerCharacterController = GetComponent<PlayerCharacterController>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerCharacterController, Jetpack>(m_PlayerCharacterController,
                    this, gameObject);
    
                m_InputHandler = GetComponent<PlayerInputHandler>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, Jetpack>(m_InputHandler, this, gameObject);
    
                CurrentFillRatio = 1f;
    
                AudioSource.clip = JetpackSfx;
                AudioSource.loop = true;
            }
    
            void Update()
            {
                // jetpack can only be used if not grounded and jump has been pressed again once in-air
                if (IsPlayergrounded())
                {
                    m_CanUseJetpack = false;
                }
                else if (!m_PlayerCharacterController.HasJumpedThisFrame && m_InputHandler.GetJumpInputDown())
                {
                    m_CanUseJetpack = true;
                }
    
                // jetpack usage
                bool jetpackIsInUse = m_CanUseJetpack && IsJetpackUnlocked && CurrentFillRatio > 0f &&
                                      m_InputHandler.GetJumpInputHeld();
                if (jetpackIsInUse)
                {
                    // store the last time of use for refill delay
                    m_LastTimeOfUse = Time.time;
    
                    float totalAcceleration = JetpackAcceleration;
    
                    // cancel out gravity
                    totalAcceleration += m_PlayerCharacterController.GravityDownForce;
    
                    if (m_PlayerCharacterController.CharacterVelocity.y < 0f)
                    {
                        // handle making the jetpack compensate for character's downward velocity with bonus acceleration
                        totalAcceleration += ((-m_PlayerCharacterController.CharacterVelocity.y / Time.deltaTime) *
                                              JetpackDownwardVelocityCancelingFactor);
                    }
    
                    // apply the acceleration to character's velocity
                    m_PlayerCharacterController.CharacterVelocity += Vector3.up * totalAcceleration * Time.deltaTime;
    
                    // consume fuel
                    CurrentFillRatio = CurrentFillRatio - (Time.deltaTime / ConsumeDuration);
    
                    for (int i = 0; i < JetpackVfx.Length; i++)
                    {
                        var emissionModulesVfx = JetpackVfx[i].emission;
                        emissionModulesVfx.enabled = true;
                    }
    
                    if (!AudioSource.isPlaying)
                        AudioSource.Play();
                }
                else
                {
                    // refill the meter over time
                    if (IsJetpackUnlocked && Time.time - m_LastTimeOfUse >= RefillDelay)
                    {
                        float refillRate = 1 / (m_PlayerCharacterController.IsGrounded
                            ? RefillDurationGrounded
                            : RefillDurationInTheAir);
                        CurrentFillRatio = CurrentFillRatio + Time.deltaTime * refillRate;
                    }
    
                    for (int i = 0; i < JetpackVfx.Length; i++)
                    {
                        var emissionModulesVfx = JetpackVfx[i].emission;
                        emissionModulesVfx.enabled = false;
                    }
    
                    // keeps the ratio between 0 and 1
                    CurrentFillRatio = Mathf.Clamp01(CurrentFillRatio);
    
                    if (AudioSource.isPlaying)
                        AudioSource.Stop();
                }
            }
    
            public bool TryUnlock()
            {
                if (IsJetpackUnlocked)
                    return false;
    
                OnUnlockJetpack.Invoke(true);
                IsJetpackUnlocked = true;
                m_LastTimeOfUse = Time.time;
                return true;
            }
        }
    }
    

    JetpackPickup

    namespace Unity.FPS.Gameplay
    {
        public class JetpackPickup : Pickup
        {
            protected override void OnPicked(PlayerCharacterController byPlayer)
            {
                var jetpack = byPlayer.GetComponent<Jetpack>();
                if (!jetpack)
                    return;
    
                if (jetpack.TryUnlock())
                {
                    PlayPickupFeedback();
                    Destroy(gameObject);
                }
            }
        }
    }
    

    OverheatBehavior

    using UnityEngine;
    using System.Collections.Generic;
    using Unity.FPS.Game;
    
    namespace Unity.FPS.Gameplay
    {
        public class OverheatBehavior : MonoBehaviour
        {
            [System.Serializable]
            public struct RendererIndexData
            {
                public Renderer Renderer;
                public int MaterialIndex;
    
                public RendererIndexData(Renderer renderer, int index)
                {
                    this.Renderer = renderer;
                    this.MaterialIndex = index;
                }
            }
    
            [Header("Visual")] [Tooltip("The VFX to scale the spawn rate based on the ammo ratio")]
            public ParticleSystem SteamVfx;
    
            [Tooltip("The emission rate for the effect when fully overheated")]
            public float SteamVfxEmissionRateMax = 8f;
    
            //Set gradient field to HDR
            [GradientUsage(true)] [Tooltip("Overheat color based on ammo ratio")]
            public Gradient OverheatGradient;
    
            [Tooltip("The material for overheating color animation")]
            public Material OverheatingMaterial;
    
            [Header("Sound")] [Tooltip("Sound played when a cell are cooling")]
            public AudioClip CoolingCellsSound;
    
            [Tooltip("Curve for ammo to volume ratio")]
            public AnimationCurve AmmoToVolumeRatioCurve;
    
    
            WeaponController m_Weapon;
            AudioSource m_AudioSource;
            List<RendererIndexData> m_OverheatingRenderersData;
            MaterialPropertyBlock m_OverheatMaterialPropertyBlock;
            float m_LastAmmoRatio;
            ParticleSystem.EmissionModule m_SteamVfxEmissionModule;
    
            void Awake()
            {
                var emissionModule = SteamVfx.emission;
                emissionModule.rateOverTimeMultiplier = 0f;
    
                m_OverheatingRenderersData = new List<RendererIndexData>();
                foreach (var renderer in GetComponentsInChildren<Renderer>(true))
                {
                    for (int i = 0; i < renderer.sharedMaterials.Length; i++)
                    {
                        if (renderer.sharedMaterials[i] == OverheatingMaterial)
                            m_OverheatingRenderersData.Add(new RendererIndexData(renderer, i));
                    }
                }
    
                m_OverheatMaterialPropertyBlock = new MaterialPropertyBlock();
                m_SteamVfxEmissionModule = SteamVfx.emission;
    
                m_Weapon = GetComponent<WeaponController>();
                DebugUtility.HandleErrorIfNullGetComponent<WeaponController, OverheatBehavior>(m_Weapon, this, gameObject);
    
                m_AudioSource = gameObject.AddComponent<AudioSource>();
                m_AudioSource.clip = CoolingCellsSound;
                m_AudioSource.outputAudioMixerGroup = AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponOverheat);
            }
    
            void Update()
            {
                // visual smoke shooting out of the gun
                float currentAmmoRatio = m_Weapon.CurrentAmmoRatio;
                if (currentAmmoRatio != m_LastAmmoRatio)
                {
                    m_OverheatMaterialPropertyBlock.SetColor("_EmissionColor",
                        OverheatGradient.Evaluate(1f - currentAmmoRatio));
    
                    foreach (var data in m_OverheatingRenderersData)
                    {
                        data.Renderer.SetPropertyBlock(m_OverheatMaterialPropertyBlock, data.MaterialIndex);
                    }
    
                    m_SteamVfxEmissionModule.rateOverTimeMultiplier = SteamVfxEmissionRateMax * (1f - currentAmmoRatio);
                }
    
                // cooling sound
                if (CoolingCellsSound)
                {
                    if (!m_AudioSource.isPlaying
                        && currentAmmoRatio != 1
                        && m_Weapon.IsWeaponActive
                        && m_Weapon.IsCooling)
                    {
                        m_AudioSource.Play();
                    }
                    else if (m_AudioSource.isPlaying
                             && (currentAmmoRatio == 1 || !m_Weapon.IsWeaponActive || !m_Weapon.IsCooling))
                    {
                        m_AudioSource.Stop();
                        return;
                    }
    
                    m_AudioSource.volume = AmmoToVolumeRatioCurve.Evaluate(1 - currentAmmoRatio);
                }
    
                m_LastAmmoRatio = currentAmmoRatio;
            }
        }
    }
    

    Pickup

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(Rigidbody), typeof(Collider))]
        public class Pickup : MonoBehaviour
        {
            [Tooltip("Frequency at which the item will move up and down")]
            public float VerticalBobFrequency = 1f;
    
            [Tooltip("Distance the item will move up and down")]
            public float BobbingAmount = 1f;
    
            [Tooltip("Rotation angle per second")] public float RotatingSpeed = 360f;
    
            [Tooltip("Sound played on pickup")] public AudioClip PickupSfx;
            [Tooltip("VFX spawned on pickup")] public GameObject PickupVfxPrefab;
    
            public Rigidbody PickupRigidbody { get; private set; }
    
            Collider m_Collider;
            Vector3 m_StartPosition;
            bool m_HasPlayedFeedback;
    
            protected virtual void Start()
            {
                PickupRigidbody = GetComponent<Rigidbody>();
                DebugUtility.HandleErrorIfNullGetComponent<Rigidbody, Pickup>(PickupRigidbody, this, gameObject);
                m_Collider = GetComponent<Collider>();
                DebugUtility.HandleErrorIfNullGetComponent<Collider, Pickup>(m_Collider, this, gameObject);
    
                // ensure the physics setup is a kinematic rigidbody trigger
                PickupRigidbody.isKinematic = true;
                m_Collider.isTrigger = true;
    
                // Remember start position for animation
                m_StartPosition = transform.position;
            }
    
            void Update()
            {
                // Handle bobbing
                float bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;
                transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;
    
                // Handle rotating
                transform.Rotate(Vector3.up, RotatingSpeed * Time.deltaTime, Space.Self);
            }
    
            void OnTriggerEnter(Collider other)
            {
                PlayerCharacterController pickingPlayer = other.GetComponent<PlayerCharacterController>();
    
                if (pickingPlayer != null)
                {
                    OnPicked(pickingPlayer);
    
                    PickupEvent evt = Events.PickupEvent;
                    evt.Pickup = gameObject;
                    EventManager.Broadcast(evt);
                }
            }
    
            protected virtual void OnPicked(PlayerCharacterController playerController)
            {
                PlayPickupFeedback();
            }
    
            public void PlayPickupFeedback()
            {
                if (m_HasPlayedFeedback)
                    return;
    
                if (PickupSfx)
                {
                    AudioUtility.CreateSFX(PickupSfx, transform.position, AudioUtility.AudioGroups.Pickup, 0f);
                }
    
                if (PickupVfxPrefab)
                {
                    var pickupVfxInstance = Instantiate(PickupVfxPrefab, transform.position, Quaternion.identity);
                }
    
                m_HasPlayedFeedback = true;
            }
        }
    }
    

    PlayerCharacterController

    using Unity.FPS.Game;
    using UnityEngine;
    using UnityEngine.Events;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(CharacterController), typeof(PlayerInputHandler), typeof(AudioSource))]
        public class PlayerCharacterController : MonoBehaviour
        {
            [Header("References")] [Tooltip("Reference to the main camera used for the player")]
            public Camera PlayerCamera;
    
            [Tooltip("Audio source for footsteps, jump, etc...")]
            public AudioSource AudioSource;
    
            [Header("General")] [Tooltip("Force applied downward when in the air")]
            public float GravityDownForce = 20f;
    
            [Tooltip("Physic layers checked to consider the player grounded")]
            public LayerMask GroundCheckLayers = -1;
    
            [Tooltip("distance from the bottom of the character controller capsule to test for grounded")]
            public float GroundCheckDistance = 0.05f;
    
            [Header("Movement")] [Tooltip("Max movement speed when grounded (when not sprinting)")]
            public float MaxSpeedOnGround = 10f;
    
            [Tooltip(
                "Sharpness for the movement when grounded, a low value will make the player accelerate and decelerate slowly, a high value will do the opposite")]
            public float MovementSharpnessOnGround = 15;
    
            [Tooltip("Max movement speed when crouching")] [Range(0, 1)]
            public float MaxSpeedCrouchedRatio = 0.5f;
    
            [Tooltip("Max movement speed when not grounded")]
            public float MaxSpeedInAir = 10f;
    
            [Tooltip("Acceleration speed when in the air")]
            public float AccelerationSpeedInAir = 25f;
    
            [Tooltip("Multiplicator for the sprint speed (based on grounded speed)")]
            public float SprintSpeedModifier = 2f;
    
            [Tooltip("Height at which the player dies instantly when falling off the map")]
            public float KillHeight = -50f;
    
            [Header("Rotation")] [Tooltip("Rotation speed for moving the camera")]
            public float RotationSpeed = 200f;
    
            [Range(0.1f, 1f)] [Tooltip("Rotation speed multiplier when aiming")]
            public float AimingRotationMultiplier = 0.4f;
    
            [Header("Jump")] [Tooltip("Force applied upward when jumping")]
            public float JumpForce = 9f;
    
            [Header("Stance")] [Tooltip("Ratio (0-1) of the character height where the camera will be at")]
            public float CameraHeightRatio = 0.9f;
    
            [Tooltip("Height of character when standing")]
            public float CapsuleHeightStanding = 1.8f;
    
            [Tooltip("Height of character when crouching")]
            public float CapsuleHeightCrouching = 0.9f;
    
            [Tooltip("Speed of crouching transitions")]
            public float CrouchingSharpness = 10f;
    
            [Header("Audio")] [Tooltip("Amount of footstep sounds played when moving one meter")]
            public float FootstepSfxFrequency = 1f;
    
            [Tooltip("Amount of footstep sounds played when moving one meter while sprinting")]
            public float FootstepSfxFrequencyWhileSprinting = 1f;
    
            [Tooltip("Sound played for footsteps")]
            public AudioClip FootstepSfx;
    
            [Tooltip("Sound played when jumping")] public AudioClip JumpSfx;
            [Tooltip("Sound played when landing")] public AudioClip LandSfx;
    
            [Tooltip("Sound played when taking damage froma fall")]
            public AudioClip FallDamageSfx;
    
            [Header("Fall Damage")]
            [Tooltip("Whether the player will recieve damage when hitting the ground at high speed")]
            public bool RecievesFallDamage;
    
            [Tooltip("Minimun fall speed for recieving fall damage")]
            public float MinSpeedForFallDamage = 10f;
    
            [Tooltip("Fall speed for recieving th emaximum amount of fall damage")]
            public float MaxSpeedForFallDamage = 30f;
    
            [Tooltip("Damage recieved when falling at the mimimum speed")]
            public float FallDamageAtMinSpeed = 10f;
    
            [Tooltip("Damage recieved when falling at the maximum speed")]
            public float FallDamageAtMaxSpeed = 50f;
    
            public UnityAction<bool> OnStanceChanged;
    
            public Vector3 CharacterVelocity { get; set; }
            public bool IsGrounded { get; private set; }
            public bool HasJumpedThisFrame { get; private set; }
            public bool IsDead { get; private set; }
            public bool IsCrouching { get; private set; }
    
            public float RotationMultiplier
            {
                get
                {
                    if (m_WeaponsManager.IsAiming)
                    {
                        return AimingRotationMultiplier;
                    }
    
                    return 1f;
                }
            }
    
            Health m_Health;
            PlayerInputHandler m_InputHandler;
            CharacterController m_Controller;
            PlayerWeaponsManager m_WeaponsManager;
            Actor m_Actor;
            Vector3 m_GroundNormal;
            Vector3 m_CharacterVelocity;
            Vector3 m_LatestImpactSpeed;
            float m_LastTimeJumped = 0f;
            float m_CameraVerticalAngle = 0f;
            float m_FootstepDistanceCounter;
            float m_TargetCharacterHeight;
    
            const float k_JumpGroundingPreventionTime = 0.2f;
            const float k_GroundCheckDistanceInAir = 0.07f;
    
            void Awake()
            {
                ActorsManager actorsManager = FindObjectOfType<ActorsManager>();
                if (actorsManager != null)
                    actorsManager.SetPlayer(gameObject);
            }
    
            void Start()
            {
                // fetch components on the same gameObject
                m_Controller = GetComponent<CharacterController>();
                DebugUtility.HandleErrorIfNullGetComponent<CharacterController, PlayerCharacterController>(m_Controller,
                    this, gameObject);
    
                m_InputHandler = GetComponent<PlayerInputHandler>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, PlayerCharacterController>(m_InputHandler,
                    this, gameObject);
    
                m_WeaponsManager = GetComponent<PlayerWeaponsManager>();
                DebugUtility.HandleErrorIfNullGetComponent<PlayerWeaponsManager, PlayerCharacterController>(
                    m_WeaponsManager, this, gameObject);
    
                m_Health = GetComponent<Health>();
                DebugUtility.HandleErrorIfNullGetComponent<Health, PlayerCharacterController>(m_Health, this, gameObject);
    
                m_Actor = GetComponent<Actor>();
                DebugUtility.HandleErrorIfNullGetComponent<Actor, PlayerCharacterController>(m_Actor, this, gameObject);
    
                m_Controller.enableOverlapRecovery = true;
    
                m_Health.OnDie += OnDie;
    
                // force the crouch state to false when starting
                SetCrouchingState(false, true);
                UpdateCharacterHeight(true);
            }
    
            void Update()
            {
                // check for Y kill
                if (!IsDead && transform.position.y < KillHeight)
                {
                    m_Health.Kill();
                }
    
                HasJumpedThisFrame = false;
    
                bool wasGrounded = IsGrounded;
                GroundCheck();
    
                // landing
                if (IsGrounded && !wasGrounded)
                {
                    // Fall damage
                    float fallSpeed = -Mathf.Min(CharacterVelocity.y, m_LatestImpactSpeed.y);
                    float fallSpeedRatio = (fallSpeed - MinSpeedForFallDamage) /
                                           (MaxSpeedForFallDamage - MinSpeedForFallDamage);
                    if (RecievesFallDamage && fallSpeedRatio > 0f)
                    {
                        float dmgFromFall = Mathf.Lerp(FallDamageAtMinSpeed, FallDamageAtMaxSpeed, fallSpeedRatio);
                        m_Health.TakeDamage(dmgFromFall, null);
    
                        // fall damage SFX
                        AudioSource.PlayOneShot(FallDamageSfx);
                    }
                    else
                    {
                        // land SFX
                        AudioSource.PlayOneShot(LandSfx);
                    }
                }
    
                // crouching
                if (m_InputHandler.GetCrouchInputDown())
                {
                    SetCrouchingState(!IsCrouching, false);
                }
    
                UpdateCharacterHeight(false);
    
                HandleCharacterMovement();
            }
    
            void OnDie()
            {
                IsDead = true;
    
                // Tell the weapons manager to switch to a non-existing weapon in order to lower the weapon
                m_WeaponsManager.SwitchToWeaponIndex(-1, true);
    
                EventManager.Broadcast(Events.PlayerDeathEvent);
            }
    
            void GroundCheck()
            {
                // Make sure that the ground check distance while already in air is very small, to prevent suddenly snapping to ground
                float chosenGroundCheckDistance =
                    IsGrounded ? (m_Controller.skinWidth + GroundCheckDistance) : k_GroundCheckDistanceInAir;
    
                // reset values before the ground check
                IsGrounded = false;
                m_GroundNormal = Vector3.up;
    
                // only try to detect ground if it's been a short amount of time since last jump; otherwise we may snap to the ground instantly after we try jumping
                if (Time.time >= m_LastTimeJumped + k_JumpGroundingPreventionTime)
                {
                    // if we're grounded, collect info about the ground normal with a downward capsule cast representing our character capsule
                    if (Physics.CapsuleCast(GetCapsuleBottomHemisphere(), GetCapsuleTopHemisphere(m_Controller.height),
                        m_Controller.radius, Vector3.down, out RaycastHit hit, chosenGroundCheckDistance, GroundCheckLayers,
                        QueryTriggerInteraction.Ignore))
                    {
                        // storing the upward direction for the surface found
                        m_GroundNormal = hit.normal;
    
                        // Only consider this a valid ground hit if the ground normal goes in the same direction as the character up
                        // and if the slope angle is lower than the character controller's limit
                        if (Vector3.Dot(hit.normal, transform.up) > 0f &&
                            IsNormalUnderSlopeLimit(m_GroundNormal))
                        {
                            IsGrounded = true;
    
                            // handle snapping to the ground
                            if (hit.distance > m_Controller.skinWidth)
                            {
                                m_Controller.Move(Vector3.down * hit.distance);
                            }
                        }
                    }
                }
            }
    
            void HandleCharacterMovement()
            {
                // horizontal character rotation
                {
                    // rotate the transform with the input speed around its local Y axis
                    transform.Rotate(
                        new Vector3(0f, (m_InputHandler.GetLookInputsHorizontal() * RotationSpeed * RotationMultiplier),
                            0f), Space.Self);
                }
    
                // vertical camera rotation
                {
                    // add vertical inputs to the camera's vertical angle
                    m_CameraVerticalAngle += m_InputHandler.GetLookInputsVertical() * RotationSpeed * RotationMultiplier;
    
                    // limit the camera's vertical angle to min/max
                    m_CameraVerticalAngle = Mathf.Clamp(m_CameraVerticalAngle, -89f, 89f);
    
                    // apply the vertical angle as a local rotation to the camera transform along its right axis (makes it pivot up and down)
                    PlayerCamera.transform.localEulerAngles = new Vector3(m_CameraVerticalAngle, 0, 0);
                }
    
                // character movement handling
                bool isSprinting = m_InputHandler.GetSprintInputHeld();
                {
                    if (isSprinting)
                    {
                        isSprinting = SetCrouchingState(false, false);
                    }
    
                    float speedModifier = isSprinting ? SprintSpeedModifier : 1f;
    
                    // converts move input to a worldspace vector based on our character's transform orientation
                    Vector3 worldspaceMoveInput = transform.TransformVector(m_InputHandler.GetMoveInput());
    
                    // handle grounded movement
                    if (IsGrounded)
                    {
                        // calculate the desired velocity from inputs, max speed, and current slope
                        Vector3 targetVelocity = worldspaceMoveInput * MaxSpeedOnGround * speedModifier;
                        // reduce speed if crouching by crouch speed ratio
                        if (IsCrouching)
                            targetVelocity *= MaxSpeedCrouchedRatio;
                        targetVelocity = GetDirectionReorientedOnSlope(targetVelocity.normalized, m_GroundNormal) *
                                         targetVelocity.magnitude;
    
                        // smoothly interpolate between our current velocity and the target velocity based on acceleration speed
                        CharacterVelocity = Vector3.Lerp(CharacterVelocity, targetVelocity,
                            MovementSharpnessOnGround * Time.deltaTime);
    
                        // jumping
                        if (IsGrounded && m_InputHandler.GetJumpInputDown())
                        {
                            // force the crouch state to false
                            if (SetCrouchingState(false, false))
                            {
                                // start by canceling out the vertical component of our velocity
                                CharacterVelocity = new Vector3(CharacterVelocity.x, 0f, CharacterVelocity.z);
    
                                // then, add the jumpSpeed value upwards
                                CharacterVelocity += Vector3.up * JumpForce;
    
                                // play sound
                                AudioSource.PlayOneShot(JumpSfx);
    
                                // remember last time we jumped because we need to prevent snapping to ground for a short time
                                m_LastTimeJumped = Time.time;
                                HasJumpedThisFrame = true;
    
                                // Force grounding to false
                                IsGrounded = false;
                                m_GroundNormal = Vector3.up;
                            }
                        }
    
                        // footsteps sound
                        float chosenFootstepSfxFrequency =
                            (isSprinting ? FootstepSfxFrequencyWhileSprinting : FootstepSfxFrequency);
                        if (m_FootstepDistanceCounter >= 1f / chosenFootstepSfxFrequency)
                        {
                            m_FootstepDistanceCounter = 0f;
                            AudioSource.PlayOneShot(FootstepSfx);
                        }
    
                        // keep track of distance traveled for footsteps sound
                        m_FootstepDistanceCounter += CharacterVelocity.magnitude * Time.deltaTime;
                    }
                    // handle air movement
                    else
                    {
                        // add air acceleration
                        CharacterVelocity += worldspaceMoveInput * AccelerationSpeedInAir * Time.deltaTime;
    
                        // limit air speed to a maximum, but only horizontally
                        float verticalVelocity = CharacterVelocity.y;
                        Vector3 horizontalVelocity = Vector3.ProjectOnPlane(CharacterVelocity, Vector3.up);
                        horizontalVelocity = Vector3.ClampMagnitude(horizontalVelocity, MaxSpeedInAir * speedModifier);
                        CharacterVelocity = horizontalVelocity + (Vector3.up * verticalVelocity);
    
                        // apply the gravity to the velocity
                        CharacterVelocity += Vector3.down * GravityDownForce * Time.deltaTime;
                    }
                }
    
                // apply the final calculated velocity value as a character movement
                Vector3 capsuleBottomBeforeMove = GetCapsuleBottomHemisphere();
                Vector3 capsuleTopBeforeMove = GetCapsuleTopHemisphere(m_Controller.height);
                m_Controller.Move(CharacterVelocity * Time.deltaTime);
    
                // detect obstructions to adjust velocity accordingly
                m_LatestImpactSpeed = Vector3.zero;
                if (Physics.CapsuleCast(capsuleBottomBeforeMove, capsuleTopBeforeMove, m_Controller.radius,
                    CharacterVelocity.normalized, out RaycastHit hit, CharacterVelocity.magnitude * Time.deltaTime, -1,
                    QueryTriggerInteraction.Ignore))
                {
                    // We remember the last impact speed because the fall damage logic might need it
                    m_LatestImpactSpeed = CharacterVelocity;
    
                    CharacterVelocity = Vector3.ProjectOnPlane(CharacterVelocity, hit.normal);
                }
            }
    
            // Returns true if the slope angle represented by the given normal is under the slope angle limit of the character controller
            bool IsNormalUnderSlopeLimit(Vector3 normal)
            {
                return Vector3.Angle(transform.up, normal) <= m_Controller.slopeLimit;
            }
    
            // Gets the center point of the bottom hemisphere of the character controller capsule    
            Vector3 GetCapsuleBottomHemisphere()
            {
                return transform.position + (transform.up * m_Controller.radius);
            }
    
            // Gets the center point of the top hemisphere of the character controller capsule    
            Vector3 GetCapsuleTopHemisphere(float atHeight)
            {
                return transform.position + (transform.up * (atHeight - m_Controller.radius));
            }
    
            // Gets a reoriented direction that is tangent to a given slope
            public Vector3 GetDirectionReorientedOnSlope(Vector3 direction, Vector3 slopeNormal)
            {
                Vector3 directionRight = Vector3.Cross(direction, transform.up);
                return Vector3.Cross(slopeNormal, directionRight).normalized;
            }
    
            void UpdateCharacterHeight(bool force)
            {
                // Update height instantly
                if (force)
                {
                    m_Controller.height = m_TargetCharacterHeight;
                    m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
                    PlayerCamera.transform.localPosition = Vector3.up * m_TargetCharacterHeight * CameraHeightRatio;
                    m_Actor.AimPoint.transform.localPosition = m_Controller.center;
                }
                // Update smooth height
                else if (m_Controller.height != m_TargetCharacterHeight)
                {
                    // resize the capsule and adjust camera position
                    m_Controller.height = Mathf.Lerp(m_Controller.height, m_TargetCharacterHeight,
                        CrouchingSharpness * Time.deltaTime);
                    m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
                    PlayerCamera.transform.localPosition = Vector3.Lerp(PlayerCamera.transform.localPosition,
                        Vector3.up * m_TargetCharacterHeight * CameraHeightRatio, CrouchingSharpness * Time.deltaTime);
                    m_Actor.AimPoint.transform.localPosition = m_Controller.center;
                }
            }
    
            // returns false if there was an obstruction
            bool SetCrouchingState(bool crouched, bool ignoreObstructions)
            {
                // set appropriate heights
                if (crouched)
                {
                    m_TargetCharacterHeight = CapsuleHeightCrouching;
                }
                else
                {
                    // Detect obstructions
                    if (!ignoreObstructions)
                    {
                        Collider[] standingOverlaps = Physics.OverlapCapsule(
                            GetCapsuleBottomHemisphere(),
                            GetCapsuleTopHemisphere(CapsuleHeightStanding),
                            m_Controller.radius,
                            -1,
                            QueryTriggerInteraction.Ignore);
                        foreach (Collider c in standingOverlaps)
                        {
                            if (c != m_Controller)
                            {
                                return false;
                            }
                        }
                    }
    
                    m_TargetCharacterHeight = CapsuleHeightStanding;
                }
    
                if (OnStanceChanged != null)
                {
                    OnStanceChanged.Invoke(crouched);
                }
    
                IsCrouching = crouched;
                return true;
            }
        }
    }
    

    PositionBobbing

    
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class PositionBobbing : MonoBehaviour
        {
            [Tooltip("Frequency at which the item will move up and down")]
            public float VerticalBobFrequency = 1f;
    
            [Tooltip("Distance the item will move up and down")]
            public float BobbingAmount = 0.5f;
    
            Vector3 m_StartPosition;
    
            void Start()
            {
                // Remember start position for animation
                m_StartPosition = transform.position;
            }
    
            void Update()
            {
                // Handle bobbing
                float bobbingAnimationPhase = ((Mathf.Sin(Time.time * VerticalBobFrequency) * 0.5f) + 0.5f) * BobbingAmount;
                transform.position = m_StartPosition + Vector3.up * bobbingAnimationPhase;
            }
        }
    }
    

    ProjectileChargeParameters

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class ProjectileChargeParameters : MonoBehaviour
        {
            public MinMaxFloat Damage;
            public MinMaxFloat Radius;
            public MinMaxFloat Speed;
            public MinMaxFloat GravityDownAcceleration;
            public MinMaxFloat AreaOfEffectDistance;
    
            ProjectileBase m_ProjectileBase;
    
            void OnEnable()
            {
                m_ProjectileBase = GetComponent<ProjectileBase>();
                DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ProjectileChargeParameters>(m_ProjectileBase,
                    this, gameObject);
    
                m_ProjectileBase.OnShoot += OnShoot;
            }
    
            void OnShoot()
            {
                // Apply the parameters based on projectile charge
                ProjectileStandard proj = GetComponent<ProjectileStandard>();
                if (proj)
                {
                    proj.Damage = Damage.GetValueFromRatio(m_ProjectileBase.InitialCharge);
                    proj.Radius = Radius.GetValueFromRatio(m_ProjectileBase.InitialCharge);
                    proj.Speed = Speed.GetValueFromRatio(m_ProjectileBase.InitialCharge);
                    proj.GravityDownAcceleration =
                        GravityDownAcceleration.GetValueFromRatio(m_ProjectileBase.InitialCharge);
                }
            }
        }
    }
    

    ProjectileStandard

    using System.Collections.Generic;
    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class ProjectileStandard : ProjectileBase
        {
            [Header("General")] [Tooltip("Radius of this projectile's collision detection")]
            public float Radius = 0.01f;
    
            [Tooltip("Transform representing the root of the projectile (used for accurate collision detection)")]
            public Transform Root;
    
            [Tooltip("Transform representing the tip of the projectile (used for accurate collision detection)")]
            public Transform Tip;
    
            [Tooltip("LifeTime of the projectile")]
            public float MaxLifeTime = 5f;
    
            [Tooltip("VFX prefab to spawn upon impact")]
            public GameObject ImpactVfx;
    
            [Tooltip("LifeTime of the VFX before being destroyed")]
            public float ImpactVfxLifetime = 5f;
    
            [Tooltip("Offset along the hit normal where the VFX will be spawned")]
            public float ImpactVfxSpawnOffset = 0.1f;
    
            [Tooltip("Clip to play on impact")] 
            public AudioClip ImpactSfxClip;
    
            [Tooltip("Layers this projectile can collide with")]
            public LayerMask HittableLayers = -1;
    
            [Header("Movement")] [Tooltip("Speed of the projectile")]
            public float Speed = 20f;
    
            [Tooltip("Downward acceleration from gravity")]
            public float GravityDownAcceleration = 0f;
    
            [Tooltip(
                "Distance over which the projectile will correct its course to fit the intended trajectory (used to drift projectiles towards center of screen in First Person view). At values under 0, there is no correction")]
            public float TrajectoryCorrectionDistance = -1;
    
            [Tooltip("Determines if the projectile inherits the velocity that the weapon's muzzle had when firing")]
            public bool InheritWeaponVelocity = false;
    
            [Header("Damage")] [Tooltip("Damage of the projectile")]
            public float Damage = 40f;
    
            [Tooltip("Area of damage. Keep empty if you don<t want area damage")]
            public DamageArea AreaOfDamage;
    
            [Header("Debug")] [Tooltip("Color of the projectile radius debug view")]
            public Color RadiusColor = Color.cyan * 0.2f;
    
            ProjectileBase m_ProjectileBase;
            Vector3 m_LastRootPosition;
            Vector3 m_Velocity;
            bool m_HasTrajectoryOverride;
            float m_ShootTime;
            Vector3 m_TrajectoryCorrectionVector;
            Vector3 m_ConsumedTrajectoryCorrectionVector;
            List<Collider> m_IgnoredColliders;
    
            const QueryTriggerInteraction k_TriggerInteraction = QueryTriggerInteraction.Collide;
    
            void OnEnable()
            {
                m_ProjectileBase = GetComponent<ProjectileBase>();
                DebugUtility.HandleErrorIfNullGetComponent<ProjectileBase, ProjectileStandard>(m_ProjectileBase, this,
                    gameObject);
    
                m_ProjectileBase.OnShoot += OnShoot;
    
                Destroy(gameObject, MaxLifeTime);
            }
    
            new void OnShoot()
            {
                m_ShootTime = Time.time;
                m_LastRootPosition = Root.position;
                m_Velocity = transform.forward * Speed;
                m_IgnoredColliders = new List<Collider>();
                transform.position += m_ProjectileBase.InheritedMuzzleVelocity * Time.deltaTime;
    
                // Ignore colliders of owner
                Collider[] ownerColliders = m_ProjectileBase.Owner.GetComponentsInChildren<Collider>();
                m_IgnoredColliders.AddRange(ownerColliders);
    
                // Handle case of player shooting (make projectiles not go through walls, and remember center-of-screen trajectory)
                PlayerWeaponsManager playerWeaponsManager = m_ProjectileBase.Owner.GetComponent<PlayerWeaponsManager>();
                if (playerWeaponsManager)
                {
                    m_HasTrajectoryOverride = true;
    
                    Vector3 cameraToMuzzle = (m_ProjectileBase.InitialPosition -
                                              playerWeaponsManager.WeaponCamera.transform.position);
    
                    m_TrajectoryCorrectionVector = Vector3.ProjectOnPlane(-cameraToMuzzle,
                        playerWeaponsManager.WeaponCamera.transform.forward);
                    if (TrajectoryCorrectionDistance == 0)
                    {
                        transform.position += m_TrajectoryCorrectionVector;
                        m_ConsumedTrajectoryCorrectionVector = m_TrajectoryCorrectionVector;
                    }
                    else if (TrajectoryCorrectionDistance < 0)
                    {
                        m_HasTrajectoryOverride = false;
                    }
    
                    if (Physics.Raycast(playerWeaponsManager.WeaponCamera.transform.position, cameraToMuzzle.normalized,
                        out RaycastHit hit, cameraToMuzzle.magnitude, HittableLayers, k_TriggerInteraction))
                    {
                        if (IsHitValid(hit))
                        {
                            OnHit(hit.point, hit.normal, hit.collider);
                        }
                    }
                }
            }
    
            void Update()
            {
                // Move
                transform.position += m_Velocity * Time.deltaTime;
                if (InheritWeaponVelocity)
                {
                    transform.position += m_ProjectileBase.InheritedMuzzleVelocity * Time.deltaTime;
                }
    
                // Drift towards trajectory override (this is so that projectiles can be centered 
                // with the camera center even though the actual weapon is offset)
                if (m_HasTrajectoryOverride && m_ConsumedTrajectoryCorrectionVector.sqrMagnitude <
                    m_TrajectoryCorrectionVector.sqrMagnitude)
                {
                    Vector3 correctionLeft = m_TrajectoryCorrectionVector - m_ConsumedTrajectoryCorrectionVector;
                    float distanceThisFrame = (Root.position - m_LastRootPosition).magnitude;
                    Vector3 correctionThisFrame =
                        (distanceThisFrame / TrajectoryCorrectionDistance) * m_TrajectoryCorrectionVector;
                    correctionThisFrame = Vector3.ClampMagnitude(correctionThisFrame, correctionLeft.magnitude);
                    m_ConsumedTrajectoryCorrectionVector += correctionThisFrame;
    
                    // Detect end of correction
                    if (m_ConsumedTrajectoryCorrectionVector.sqrMagnitude == m_TrajectoryCorrectionVector.sqrMagnitude)
                    {
                        m_HasTrajectoryOverride = false;
                    }
    
                    transform.position += correctionThisFrame;
                }
    
                // Orient towards velocity
                transform.forward = m_Velocity.normalized;
    
                // Gravity
                if (GravityDownAcceleration > 0)
                {
                    // add gravity to the projectile velocity for ballistic effect
                    m_Velocity += Vector3.down * GravityDownAcceleration * Time.deltaTime;
                }
    
                // Hit detection
                {
                    RaycastHit closestHit = new RaycastHit();
                    closestHit.distance = Mathf.Infinity;
                    bool foundHit = false;
    
                    // Sphere cast
                    Vector3 displacementSinceLastFrame = Tip.position - m_LastRootPosition;
                    RaycastHit[] hits = Physics.SphereCastAll(m_LastRootPosition, Radius,
                        displacementSinceLastFrame.normalized, displacementSinceLastFrame.magnitude, HittableLayers,
                        k_TriggerInteraction);
                    foreach (var hit in hits)
                    {
                        if (IsHitValid(hit) && hit.distance < closestHit.distance)
                        {
                            foundHit = true;
                            closestHit = hit;
                        }
                    }
    
                    if (foundHit)
                    {
                        // Handle case of casting while already inside a collider
                        if (closestHit.distance <= 0f)
                        {
                            closestHit.point = Root.position;
                            closestHit.normal = -transform.forward;
                        }
    
                        OnHit(closestHit.point, closestHit.normal, closestHit.collider);
                    }
                }
    
                m_LastRootPosition = Root.position;
            }
    
            bool IsHitValid(RaycastHit hit)
            {
                // ignore hits with an ignore component
                if (hit.collider.GetComponent<IgnoreHitDetection>())
                {
                    return false;
                }
    
                // ignore hits with triggers that don't have a Damageable component
                if (hit.collider.isTrigger && hit.collider.GetComponent<Damageable>() == null)
                {
                    return false;
                }
    
                // ignore hits with specific ignored colliders (self colliders, by default)
                if (m_IgnoredColliders != null && m_IgnoredColliders.Contains(hit.collider))
                {
                    return false;
                }
    
                return true;
            }
    
            void OnHit(Vector3 point, Vector3 normal, Collider collider)
            {
                // damage
                if (AreaOfDamage)
                {
                    // area damage
                    AreaOfDamage.InflictDamageInArea(Damage, point, HittableLayers, k_TriggerInteraction,
                        m_ProjectileBase.Owner);
                }
                else
                {
                    // point damage
                    Damageable damageable = collider.GetComponent<Damageable>();
                    if (damageable)
                    {
                        damageable.InflictDamage(Damage, false, m_ProjectileBase.Owner);
                    }
                }
    
                // impact vfx
                if (ImpactVfx)
                {
                    GameObject impactVfxInstance = Instantiate(ImpactVfx, point + (normal * ImpactVfxSpawnOffset),
                        Quaternion.LookRotation(normal));
                    if (ImpactVfxLifetime > 0)
                    {
                        Destroy(impactVfxInstance.gameObject, ImpactVfxLifetime);
                    }
                }
    
                // impact sfx
                if (ImpactSfxClip)
                {
                    AudioUtility.CreateSFX(ImpactSfxClip, point, AudioUtility.AudioGroups.Impact, 1f, 3f);
                }
    
                // Self Destruct
                Destroy(this.gameObject);
            }
    
            void OnDrawGizmosSelected()
            {
                Gizmos.color = RadiusColor;
                Gizmos.DrawSphere(transform.position, Radius);
            }
        }
    }
    

    TeleportPlayer

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        // Debug script, teleports the player across the map for faster testing
        public class TeleportPlayer : MonoBehaviour
        {
            public KeyCode ActivateKey = KeyCode.F12;
    
            PlayerCharacterController m_PlayerCharacterController;
    
            void Awake()
            {
                m_PlayerCharacterController = FindObjectOfType<PlayerCharacterController>();
                DebugUtility.HandleErrorIfNullFindObject<PlayerCharacterController, TeleportPlayer>(
                    m_PlayerCharacterController, this);
            }
    
            void Update()
            {
                if (Input.GetKeyDown(ActivateKey))
                {
                    m_PlayerCharacterController.transform.SetPositionAndRotation(transform.position, transform.rotation);
                    Health playerHealth = m_PlayerCharacterController.GetComponent<Health>();
                    if (playerHealth)
                    {
                        playerHealth.Heal(999);
                    }
                }
            }
    
        }
    }
    

    WeaponFuelCellHandler

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        [RequireComponent(typeof(WeaponController))]
        public class WeaponFuelCellHandler : MonoBehaviour
        {
            [Tooltip("Retract All Fuel Cells Simultaneously")]
            public bool SimultaneousFuelCellsUsage = false;
    
            [Tooltip("List of GameObjects representing the fuel cells on the weapon")]
            public GameObject[] FuelCells;
    
            [Tooltip("Cell local position when used")]
            public Vector3 FuelCellUsedPosition;
    
            [Tooltip("Cell local position before use")]
            public Vector3 FuelCellUnusedPosition = new Vector3(0f, -0.1f, 0f);
    
            WeaponController m_Weapon;
            bool[] m_FuelCellsCooled;
    
            void Start()
            {
                m_Weapon = GetComponent<WeaponController>();
                DebugUtility.HandleErrorIfNullGetComponent<WeaponController, WeaponFuelCellHandler>(m_Weapon, this,
                    gameObject);
    
                m_FuelCellsCooled = new bool[FuelCells.Length];
                for (int i = 0; i < m_FuelCellsCooled.Length; i++)
                {
                    m_FuelCellsCooled[i] = true;
                }
            }
    
            void Update()
            {
                if (SimultaneousFuelCellsUsage)
                {
                    for (int i = 0; i < FuelCells.Length; i++)
                    {
                        FuelCells[i].transform.localPosition = Vector3.Lerp(FuelCellUsedPosition, FuelCellUnusedPosition,
                            m_Weapon.CurrentAmmoRatio);
                    }
                }
                else
                {
                    // TODO: needs simplification
                    for (int i = 0; i < FuelCells.Length; i++)
                    {
                        float length = FuelCells.Length;
                        float lim1 = i / length;
                        float lim2 = (i + 1) / length;
    
                        float value = Mathf.InverseLerp(lim1, lim2, m_Weapon.CurrentAmmoRatio);
                        value = Mathf.Clamp01(value);
    
                        FuelCells[i].transform.localPosition =
                            Vector3.Lerp(FuelCellUsedPosition, FuelCellUnusedPosition, value);
                    }
                }
            }
        }
    }
    

    WeaponPickup

    using Unity.FPS.Game;
    using UnityEngine;
    
    namespace Unity.FPS.Gameplay
    {
        public class WeaponPickup : Pickup
        {
            [Tooltip("The prefab for the weapon that will be added to the player on pickup")]
            public WeaponController WeaponPrefab;
    
            protected override void Start()
            {
                base.Start();
    
                // Set all children layers to default (to prefent seeing weapons through meshes)
                foreach (Transform t in GetComponentsInChildren<Transform>())
                {
                    if (t != transform)
                        t.gameObject.layer = 0;
                }
            }
    
            protected override void OnPicked(PlayerCharacterController byPlayer)
            {
                PlayerWeaponsManager playerWeaponsManager = byPlayer.GetComponent<PlayerWeaponsManager>();
                if (playerWeaponsManager)
                {
                    if (playerWeaponsManager.AddWeapon(WeaponPrefab))
                    {
                        // Handle auto-switching to weapon if no weapons currently
                        if (playerWeaponsManager.GetActiveWeapon() == null)
                        {
                            playerWeaponsManager.SwitchWeapon(true);
                        }
    
                        PlayPickupFeedback();
                        Destroy(gameObject);
                    }
                }
            }
        }
    }
    

    三、效果展示



    相关文章

      网友评论

          本文标题:OpenXR开发实战项目之VR FPS射击

          本文链接:https://www.haomeiwen.com/subject/uivbidtx.html