/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Items.Actions { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Inventory; using Opsive.UltimateCharacterController.Audio; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Character.Abilities.Items; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Inventory; using Opsive.UltimateCharacterController.Items.Actions.PerspectiveProperties; using Opsive.UltimateCharacterController.Items.AnimatorAudioStates; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking.Game; #endif using Opsive.UltimateCharacterController.Objects; using Opsive.UltimateCharacterController.Objects.ItemAssist; using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.SurfaceSystem; using Opsive.UltimateCharacterController.Traits; using Opsive.UltimateCharacterController.Utility; using System.Collections.Generic; using UnityEngine; /// /// Any weapon that can shoot. This includes pistols, rocket launchers, bow and arrows, etc. /// public class ShootableWeapon : UsableItem, IReloadableItem { /// /// The mode in which the weapon fires multiple shots. /// public enum FireMode { SemiAuto, // Fire discrete shots, don't continue to fire until the player fires again. FullAuto, // Keep firing until the ammo runs out or the player stops firing. Burst // Keep firing until the burst rate is zero. } /// /// Specifies how the weapon should be fired. /// public enum FireType { Instant, // Fire the shot immediately. ChargeAndFire, // Wait for the Used callback and then fire. ChargeAndHold // Fire as soon as the Use button is released. } /// /// Specifies when the projectile should become visible. /// public enum ProjectileVisiblityType { OnFire, // The projectile is only visible when the weapon is being fired. OnAim, // The projectile is visible when the character is aiming. OnReload, //The projectile is only visible when reloading. Always // The projectile is always visible when the item is equipped. } /// /// Specifies the current status of the shown projectile. /// private enum ShowProjectileStatus { NotShown, // The projectile is not currently shown. AttachmentLocation, // The projectile is parented to the attachment location. FirePointLocation // The projectile is parented to the fire point location. } /// /// Specifies how the clip should be reloaded. /// public enum ReloadClipType { Full, // Reload the entire clip. Single // Reload a single bullet. } [Tooltip("The ItemIdentifier that is consumed by the item.")] [UnityEngine.Serialization.FormerlySerializedAs("m_ConsumableItemType")] [SerializeField] protected ItemDefinitionBase m_ConsumableItemDefinition; [Tooltip("The mode in which the weapon fires multiple shots.")] [SerializeField] protected FireMode m_FireMode = FireMode.SemiAuto; [Tooltip("Specifies when the weapon should be fired.")] [SerializeField] protected FireType m_FireType = FireType.Instant; [Tooltip("If using a charge FireType, the minimum amount of time that the weapon must be charged for in order to be fired.")] [SerializeField] protected float m_MinChargeLength; [Tooltip("If using a charge FireType, the amount of time that the weapon must be charged for in order to be fully fired.")] [SerializeField] protected float m_FullChargeLength; [Tooltip("The Item Substate parameter value when charging.")] [SerializeField] protected int m_ChargeItemSubstateParameterValue = 10; [Tooltip("If using a charge FireType, the minimum amount strength that the weapon will use if it isn't fully charged.")] [Range(0, 1)] [SerializeField] protected float m_MinChargeStrength = 0.5f; [Tooltip("A set of AudioClips that can be played when the weapon is charging.")] [SerializeField] protected AudioClipSet m_ChargeAudioClipSet = new AudioClipSet(); [Tooltip("The number of rounds to fire in a single shot.")] [SerializeField] protected int m_FireCount = 1; [Tooltip("If using the Burst FireMode, specifies the number of bursts the weapon can fire.")] [SerializeField] protected int m_BurstCount = 5; [Tooltip("If using the Burst FireMode, specifies the delay before the next burst can occur.")] [SerializeField] protected float m_BurstDelay = 0.25f; [Tooltip("The random spread of the bullets once they are fired.")] [Range(0, 360)] [SerializeField] protected float m_Spread = 0.01f; [Tooltip("Should the weapon fire in the LookSource direction? If false the weapon will be fired in the direction of the weapon.")] [SerializeField] protected bool m_FireInLookSourceDirection = true; [Tooltip("Optionally specify a projectile that the weapon should use.")] [SerializeField] protected GameObject m_Projectile; [Tooltip("The magnitude of the projectile velocity when fired. The direction is determined by the fire direction.")] [SerializeField] protected float m_ProjectileFireVelocityMagnitude = 10; [Tooltip("Specifies when the projectile should become visible.")] [SerializeField] protected ProjectileVisiblityType m_ProjectileVisibility = ProjectileVisiblityType.OnFire; [Tooltip("The layer that the projectile should occupy when initially spawned.")] [SerializeField] protected int m_ProjectileStartLayer = LayerManager.IgnoreRaycast; [Tooltip("The layer that the projectile object should change to after being fired.")] [SerializeField] protected int m_ProjectileFiredLayer = LayerManager.VisualEffect; [Tooltip("The amount of time after the object has been fired to change the layer.")] [SerializeField] protected float m_LayerChangeDelay = 0.1f; [Tooltip("The amount of time after another item has been used that the projectile should be enabled again.")] [SerializeField] protected float m_ProjectileEnableDelayAfterOtherUse = 0.4f; [Tooltip("The Item Substate parameter value when the weapon tries to fire but is out of ammo.")] [SerializeField] protected int m_DryFireItemSubstateParameterValue = 11; [Tooltip("A set of AudioClips that can be played when the weapon is out of ammo.")] [SerializeField] protected AudioClipSet m_DryFireAudioClipSet = new AudioClipSet(); [Tooltip("The delay until the hitscan is performed after being fired. Set to 0 to fire immediately.")] [SerializeField] protected float m_HitscanFireDelay = 0; [Tooltip("The maximum distance in which the hitscan fire can reach.")] [SerializeField] protected float m_HitscanFireRange = float.MaxValue; [Tooltip("The maximum number of objects the hitscan cast can collide with.")] [SerializeField] protected int m_MaxHitscanCollisionCount = 15; [Tooltip("A LayerMask of the layers that can be hit when fired at.")] [SerializeField] protected LayerMask m_ImpactLayers = ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.TransparentFX | 1 << LayerManager.UI | 1 << LayerManager.Overlay); [Tooltip("Specifies if the hitscan can detect triggers.")] [SerializeField] protected QueryTriggerInteraction m_HitscanTriggerInteraction = QueryTriggerInteraction.Ignore; [Tooltip("The amount of damage to apply to the hit object.")] [SerializeField] protected float m_DamageAmount = 10; [Tooltip("The amount of force to apply to the hit object.")] [SerializeField] protected float m_ImpactForce = 2; [Tooltip("The number of frames to add the impact force to.")] [SerializeField] protected int m_ImpactForceFrames = 15; [Tooltip("The name of the state to activate upon impact.")] [SerializeField] protected string m_ImpactStateName; [Tooltip("The number of seconds until the impact state is disabled. A value of -1 will require the state to be disabled manually.")] [SerializeField] protected float m_ImpactStateDisableTimer = 10; [Tooltip("The Surface Impact triggered when the weapon hits an object.")] [SerializeField] protected SurfaceImpact m_SurfaceImpact; [Tooltip("The number of rounds in the clip.")] [SerializeField] protected int m_ClipSize = 50; [Tooltip("Specifies when the item should be automatically reloaded.")] [SerializeField] protected Reload.AutoReloadType m_AutoReload = Reload.AutoReloadType.Pickup | Reload.AutoReloadType.Empty; [Tooltip("Specifies how the clip should be reloaded.")] [SerializeField] protected ReloadClipType m_ReloadType = ReloadClipType.Full; [Tooltip("Can the camera zoom during a reload?")] [SerializeField] protected bool m_ReloadCanCameraZoom = true; [Tooltip("Should the crosshairs spread during a recoil?")] [SerializeField] protected bool m_ReloadCrosshairsSpread = true; [Tooltip("Specifies the animator and audio state from a reload.")] [SerializeField] protected AnimatorAudioStateSet m_ReloadAnimatorAudioStateSet = new AnimatorAudioStateSet(); [Tooltip("Specifies if the item should wait for the OnAnimatorItemReload animation event or wait for the specified duration before reloading.")] [SerializeField] protected AnimationEventTrigger m_ReloadEvent = new AnimationEventTrigger(true, 0); [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadComplete animation event or wait for the specified duration before completing the reload.")] [SerializeField] protected AnimationEventTrigger m_ReloadCompleteEvent = new AnimationEventTrigger(true, 0); [Tooltip("The clip that should be played after the item has finished reloading.")] [SerializeField] protected AudioClipSet m_ReloadCompleteAudioClipSet; [Tooltip("Should the weapon clip be detached and attached when reloaded?")] [SerializeField] protected bool m_ReloadDetachAttachClip = false; [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadDetachClip animation event or wait for the specified duration before detaching the clip from the weapon.")] [SerializeField] protected AnimationEventTrigger m_ReloadDetachClipEvent = new AnimationEventTrigger(true, 0.4f); [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadShowProjectile animation event or wait for the specified duration before showing the projectile.")] [SerializeField] protected AnimationEventTrigger m_ReloadShowProjectileEvent = new AnimationEventTrigger(true, 0.4f); [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadAttachProjectile animation event or wait for the specified duration before parenting the projectile to the fire point.")] [SerializeField] protected AnimationEventTrigger m_ReloadAttachProjectileEvent = new AnimationEventTrigger(true, 0.6f); [Tooltip("The prefab that is dropped when the character is reloading.")] [SerializeField] protected GameObject m_ReloadDropClip; [Tooltip("The amount of time after the clip has been removed to change the layer.")] [SerializeField] protected float m_ReloadClipLayerChangeDelay = 0.1f; [Tooltip("The layer that the clip object should change to after being reloaded.")] [SerializeField] protected int m_ReloadClipTargetLayer = LayerManager.VisualEffect; [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadDropClip animation event or wait for the specified duration before dropping the clip from the weapon.")] [SerializeField] protected AnimationEventTrigger m_ReloadDropClipEvent = new AnimationEventTrigger(true, 0.1f); [Tooltip("Specifies if the item should wait for the OnAnimatorItemReloadAttachClip animation event or wait for the specified duration before attaching the clip back to the weapon.")] [SerializeField] protected AnimationEventTrigger m_ReloadAttachClipEvent = new AnimationEventTrigger(true, 0.4f); [Tooltip("The amount of positional recoil to add to the item.")] [SerializeField] protected MinMaxVector3 m_PositionRecoil = new MinMaxVector3(new Vector3(0, 0, -0.3f), new Vector3(0, 0.01f, -0.1f)); [Tooltip("The amount of rotational recoil to add to the item.")] [SerializeField] protected MinMaxVector3 m_RotationRecoil = new MinMaxVector3(new Vector3(-0.5f, -0.5f, 0), new Vector3(0.5f, 0.5f, 0)); [Tooltip("The amount of positional recoil to add to the camera.")] [SerializeField] protected MinMaxVector3 m_PositionCameraRecoil; [Tooltip("The amount of rotational recoil to add to the camera.")] [SerializeField] protected MinMaxVector3 m_RotationCameraRecoil = new MinMaxVector3(new Vector3(-2f, -1f, 0), new Vector3(-1f, 1f, 0)); [Tooltip("The percent of the recoil to accumulate to the camera's rest value.")] [Range(0, 1)] [SerializeField] protected float m_CameraRecoilAccumulation; [Tooltip("Is the recoil force localized to the direct parent?")] [SerializeField] protected bool m_LocalizeRecoilForce = false; [Tooltip("A reference to the muzzle flash prefab.")] [SerializeField] protected GameObject m_MuzzleFlash; [Tooltip("Should the muzzle flash be pooled? If false a single muzzle flash object will be used.")] [SerializeField] protected bool m_PoolMuzzleFlash = true; [Tooltip("A reference to the shell prefab.")] [SerializeField] protected GameObject m_Shell; [Tooltip("The velocity that the shell should eject at.")] [SerializeField] protected MinMaxVector3 m_ShellVelocity = new MinMaxVector3(new Vector3(3, 0, 0), new Vector3(4, 2, 0)); [Tooltip("The torque that the projectile should initialize with.")] [SerializeField] protected MinMaxVector3 m_ShellTorque = new MinMaxVector3(new Vector3(-50, -50, -50), new Vector3(50, 50, 50)); [Tooltip("Eject the shell after the specified delay.")] [SerializeField] protected float m_ShellEjectDelay; [Tooltip("A reference to the smoke prefab.")] [SerializeField] protected GameObject m_Smoke; [Tooltip("Spawn the smoke after the specified delay.")] [SerializeField] protected float m_SmokeSpawnDelay; [Tooltip("Optionally specify a tracer that should should appear when the hitscan weapon is fired.")] [SerializeField] protected GameObject m_Tracer; [Tooltip("The length of the tracer if no target is hit.")] [SerializeField] protected float m_TracerDefaultLength = 100; [Tooltip("Spawn the tracer after the specified delay.")] [SerializeField] protected float m_TracerSpawnDelay; [Tooltip("Should the camera's scope camera be disabled when the character isn't aiming?")] [SerializeField] protected bool m_DisableScopeCameraOnNoAim; [Tooltip("Unity event invoked when the hitscan hits another object.")] [SerializeField] protected UnityFloatVector3Vector3GameObjectEvent m_OnHitscanImpactEvent; public ItemDefinitionBase ConsumableItemDefinition { get { return m_ConsumableItemDefinition; } set { m_ConsumableItemDefinition = value; } } public FireMode Mode { get { return m_FireMode; } set { m_FireMode = value; } } public FireType Type { get { return m_FireType; } set { m_FireType = value; } } public float MinChargeLength { get { return m_MinChargeLength; } set { m_MinChargeLength = value; } } public float FullChargeLength { get { return m_FullChargeLength; } set { m_FullChargeLength = value; } } public int ChargeItemSubstateParameterValue { get { return m_ChargeItemSubstateParameterValue; } set { m_ChargeItemSubstateParameterValue = value; } } public float MinChargeStrength { get { return m_MinChargeStrength; } set { m_MinChargeStrength = value; } } public AudioClipSet ChargeAudioClipSet { get { return m_ChargeAudioClipSet; } set { m_ChargeAudioClipSet = value; } } public int FireCount { get { return m_FireCount; } set { m_FireCount = value; } } public int BurstCount { get { return m_BurstCount; } set { m_BurstCount = value; } } public float BurstDelay { get { return m_BurstDelay; } set { m_BurstDelay = value; } } public float Spread { get { return m_Spread; } set { m_Spread = value; } } public bool FireInLookSourceDirection { get { return m_FireInLookSourceDirection; } set { m_FireInLookSourceDirection = value; } } public GameObject Projectile { get { return m_Projectile; } set { m_Projectile = value; } } public float ProjectileFireVelocityMagnitude { get { return m_ProjectileFireVelocityMagnitude; } set { m_ProjectileFireVelocityMagnitude = value; } } public ProjectileVisiblityType ProjectileVisiblity { get { return m_ProjectileVisibility; } set { m_ProjectileVisibility = value; } } public int ProjectileStartLayer { get { return m_ProjectileStartLayer; } set { m_ProjectileStartLayer = value; if (m_SpawnedProjectile != null) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif m_SpawnedProjectile.layer = m_ProjectileStartLayer; } } } public int ProjectileFiredLayer { get { return m_ProjectileFiredLayer; } set { m_ProjectileFiredLayer = value; } } public float LayerChangeDelay { get { return m_LayerChangeDelay; } set { m_LayerChangeDelay = value; } } public float ProjectileEnableDelayAfterOtherUse { get { return m_ProjectileEnableDelayAfterOtherUse; } set { m_ProjectileEnableDelayAfterOtherUse = value; } } public int DryFireItemSubstateParameterValue { get { return m_DryFireItemSubstateParameterValue; } set { m_DryFireItemSubstateParameterValue = value; } } public AudioClipSet DryFireAudioClipSet { get { return m_DryFireAudioClipSet; } set { m_DryFireAudioClipSet = value; } } public float HitscanFireDelay { get { return m_HitscanFireDelay; } set { m_HitscanFireDelay = value; } } public float HitscanFireRange { get { return m_HitscanFireRange; } set { m_HitscanFireRange = value; } } public LayerMask ImpactLayers { get { return m_ImpactLayers; } set { m_ImpactLayers = value; } } public QueryTriggerInteraction HitscanTriggerInteraction { get { return m_HitscanTriggerInteraction; } set { m_HitscanTriggerInteraction = value; } } public float DamageAmount { get { return m_DamageAmount; } set { m_DamageAmount = value; } } public float ImpactForce { get { return m_ImpactForce; } set { m_ImpactForce = value; } } public int ImpactForceFrames { get { return m_ImpactForceFrames; } set { m_ImpactForceFrames = value; } } public string ImpactStateName { get { return m_ImpactStateName; } set { m_ImpactStateName = value; } } public float ImpactStateDisableTimer { get { return m_ImpactStateDisableTimer; } set { m_ImpactStateDisableTimer = value; } } public SurfaceImpact SurfaceImpact { get { return m_SurfaceImpact; } set { m_SurfaceImpact = value; } } public int ClipSize { get { return m_ClipSize; } set { m_ClipSize = value; } } public Reload.AutoReloadType AutoReload { get { return m_AutoReload; } set { m_AutoReload = value; } } public ReloadClipType ReloadType { get { return m_ReloadType; } set { m_ReloadType = value; } } public bool CanCameraZoom { get { return m_ReloadCanCameraZoom; } set { m_ReloadCanCameraZoom = value; } } public bool ReloadCrosshairsSpread { get { return m_ReloadCrosshairsSpread; } set { m_ReloadCrosshairsSpread = value; } } public AnimatorAudioStateSet ReloadAnimatorAudioStateSet { get { return m_ReloadAnimatorAudioStateSet; } set { m_ReloadAnimatorAudioStateSet = value; } } public AnimationEventTrigger ReloadEvent { get { return m_ReloadEvent; } set { m_ReloadEvent = value; } } public AnimationEventTrigger ReloadCompleteEvent { get { return m_ReloadCompleteEvent; } set { m_ReloadCompleteEvent = value; } } public AudioClipSet ReloadCompleteAudioClipSet { get { return m_ReloadCompleteAudioClipSet; } set { m_ReloadCompleteAudioClipSet = value; } } public bool ReloadDetachAttachClip { get { return m_ReloadDetachAttachClip; } set { if (m_Reloading && m_ReloadDetachAttachClip && !value) { #if FIRST_PERSON_CONTROLLER DetachAttachClip(true, m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, true); #endif DetachAttachClip(true, m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, false); } m_ReloadDetachAttachClip = value; } } public AnimationEventTrigger ReloadDetachClipEvent { get { return m_ReloadDetachClipEvent; } set { m_ReloadDetachClipEvent = value; } } public AnimationEventTrigger ReloadShowProjectileEvent { get { return m_ReloadShowProjectileEvent; } set { m_ReloadShowProjectileEvent = value; } } public AnimationEventTrigger ReloadAttachProjectileEvent { get { return m_ReloadAttachProjectileEvent; } set { m_ReloadAttachProjectileEvent = value; } } public GameObject ReloadDropClip { get { return m_ReloadDropClip; } set { m_ReloadDropClip = value; } } public float ReloadClipLayerChangeDelay { get { return m_ReloadClipLayerChangeDelay; } set { m_ReloadClipLayerChangeDelay = value; } } public int ReloadClipTargetLayer { get { return m_ReloadClipTargetLayer; } set { m_ReloadClipTargetLayer = value; } } public AnimationEventTrigger ReloadDropClipEvent { get { return m_ReloadDropClipEvent; } set { m_ReloadDropClipEvent = value; } } public AnimationEventTrigger ReloadAttachClipEvent { get { return m_ReloadAttachClipEvent; } set { m_ReloadAttachClipEvent = value; } } public MinMaxVector3 PositionRecoil { get { return m_PositionRecoil; } set { m_PositionRecoil = value; } } public MinMaxVector3 RotationRecoil { get { return m_RotationRecoil; } set { m_RotationRecoil = value; } } public MinMaxVector3 PositionCameraRecoil { get { return m_PositionCameraRecoil; } set { m_PositionCameraRecoil = value; } } public MinMaxVector3 RotationCameraRecoil { get { return m_RotationCameraRecoil; } set { m_RotationCameraRecoil = value; } } public float CameraRecoilAccumulation { get { return m_CameraRecoilAccumulation; } set { m_CameraRecoilAccumulation = value; } } public bool LocalizeRecoilForce { get { return m_LocalizeRecoilForce; } set { m_LocalizeRecoilForce = value; } } public GameObject MuzzleFlash { get { return m_MuzzleFlash; } set { m_MuzzleFlash = value; } } public bool PoolMuzzleFlash { get { return m_PoolMuzzleFlash; } set { m_PoolMuzzleFlash = value; } } public GameObject Shell { get { return m_Shell; } set { m_Shell = value; } } public MinMaxVector3 ShellVelocity { get { return m_ShellVelocity; } set { m_ShellVelocity = value; } } public MinMaxVector3 ShellTorque { get { return m_ShellTorque; } set { m_ShellTorque = value; } } public float ShellEjectDelay { get { return m_ShellEjectDelay; } set { m_ShellEjectDelay = value; } } public GameObject Smoke { get { return m_Smoke; } set { m_Smoke = value; } } public float SmokeSpawnDelay { get { return m_SmokeSpawnDelay; } set { m_SmokeSpawnDelay = value; } } public GameObject Tracer { get { return m_Tracer; } set { m_Tracer = value; } } public float TracerDefaultLength { get { return m_TracerDefaultLength; } set { m_TracerDefaultLength = value; } } public float TracerSpawnDelay { get { return m_TracerSpawnDelay; } set { m_TracerSpawnDelay = value; } } public bool DisableScopeCameraOnNoAim { get { return m_DisableScopeCameraOnNoAim; } set { m_DisableScopeCameraOnNoAim = value; } } public UnityFloatVector3Vector3GameObjectEvent OnHitscanImpactEvent { get { return m_OnHitscanImpactEvent; } set { m_OnHitscanImpactEvent = value; } } private UltimateCharacterLocomotion m_CharacterLocomotion; private Transform m_CharacterTransform; private IItemIdentifier m_ConsumableItemIdentifier; private bool m_FacingTarget; private bool m_Firing; private bool m_FireModeCanUse = true; private int m_ClipRemaining; private int m_UseCount; private float m_StartChargeTime = -1; private bool m_WaitForCharge; private bool m_Charged; private ScheduledEventBase m_ChargeEvent; private ScheduledEventBase m_BurstEvent; private ScheduledEventBase m_ReloadDetachAttachClipEvent; private ScheduledEventBase m_ReloadClipEvent; private ScheduledEventBase m_ReloadProjectileEvent; private GameObject m_SpawnedProjectile; private RaycastHit m_RaycastHit; private RaycastHit[] m_HitscanRaycastHits; private UnityEngineUtility.RaycastHitComparer m_RaycastHitComparer = new UnityEngineUtility.RaycastHitComparer(); private IShootableWeaponPerspectiveProperties m_ShootableWeaponPerspectiveProperties; private GameObject m_SpawnedMuzzleFlash; private bool m_Aiming; private bool m_Reloading; private bool m_SharedConsumableItemIdentifier; private bool m_ReloadInitialized; private bool m_TryStopUse; private bool m_CanPlayDryFireAnimation; private bool m_CanPlayDryFireAudio = true; private int m_ActiveSharedConsumableItemIdentifierAmount; private Dictionary m_SharedConsumableItemIdentifierAmountMap; private int m_TotalReloadAmount; private int m_TotalClipAmount; private int m_TotalConsumableAmount; private ShowProjectileStatus m_ShowReloadProjectile; private Transform m_FirstPersonReloadableClipParent; private Vector3 m_FirstPersonReloadableClipLocalPosition; private Quaternion m_FirstPersonReloadableClipLocalRotation; private Transform m_ThirdPersonReloadableClipParent; private Vector3 m_ThirdPersonReloadableClipLocalPosition; private Quaternion m_ThirdPersonReloadableClipLocalRotation; private GameObject m_FirstPersonReloadAddClip; private GameObject m_ThirdPersonReloadAddClip; public int ClipRemaining { get { return m_ClipRemaining; } private set { m_ClipRemaining = value; EventHandler.ExecuteEvent(m_Character, "OnItemUseConsumableItemIdentifier", m_Item, m_ConsumableItemIdentifier, m_ClipRemaining); } } public IShootableWeaponPerspectiveProperties ShootableWeaponPerspectiveProperties { get { return m_ShootableWeaponPerspectiveProperties; } } /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_CharacterLocomotion = m_Character.GetCachedComponent(); m_CharacterTransform = m_Character.transform; if (m_ConsumableItemDefinition != null) { m_ConsumableItemIdentifier = m_ConsumableItemDefinition.CreateItemIdentifier(); } #if FIRST_PERSON_CONTROLLER if (m_Item.FirstPersonPerspectiveItem != null) { AudioManager.SetReserveCount(m_Item.FirstPersonPerspectiveItem.GetVisibleObject(), 1); } #endif if (m_Item.ThirdPersonPerspectiveItem != null) { AudioManager.SetReserveCount(m_Item.ThirdPersonPerspectiveItem.GetVisibleObject(), 1); } m_ReloadAnimatorAudioStateSet.DeserializeAnimatorAudioStateSelector(m_Item, m_CharacterLocomotion); m_ReloadAnimatorAudioStateSet.Awake(m_Item.gameObject); m_ShootableWeaponPerspectiveProperties = m_ActivePerspectiveProperties as IShootableWeaponPerspectiveProperties; m_HitscanRaycastHits = new RaycastHit[m_MaxHitscanCollisionCount]; EventHandler.RegisterEvent(m_Character, "OnAimAbilityStart", OnAim); EventHandler.RegisterEvent(m_Character, "OnInventoryAddItem", OnAddItem); EventHandler.RegisterEvent(m_Character, "OnInventoryEquipItem", OnEquipItem); EventHandler.RegisterEvent(m_Character, "OnInventoryUnequipItem", OnUnequipItem); #if FIRST_PERSON_CONTROLLER && !FIRST_PERSON_SHOOTER Debug.LogError("Error: The first person perspective is imported but the first person shooter weapons do not exist. Ensure the First Person Controller or UFPS is imported."); #endif } /// /// Initializes any values that require on other components to first initialize. /// protected override void Start() { base.Start(); if (m_ShootableWeaponPerspectiveProperties == null) { m_ShootableWeaponPerspectiveProperties = m_ActivePerspectiveProperties as IShootableWeaponPerspectiveProperties; if (m_ShootableWeaponPerspectiveProperties == null) { Debug.LogError($"Error: The First/Third Person Shootable Weapon Properties component cannot be found for the Item {name}." + $"Ensure the component exists and the component's Action ID matches the Action ID of the Item ({m_ID})."); } } } /// /// The item will be equipped. /// public override void WillEquip() { var returnedConsumable = false; // When the weapon is equipped for the first time it needs to reload so it doesn't need to be reloaded manually. if (!m_ReloadInitialized) { ItemSetManagerBase itemSetManager; // Consumable ItemIdentifiers that are shared may need to have their amount redistrubuted for on the first equip. This will allow all items to have an equal // amount on the first run. if (m_SharedConsumableItemIdentifier && (itemSetManager = m_Inventory.GetComponent()) != null) { for (int i = 0; i < m_Inventory.SlotCount; ++i) { // Use the ItemSetManager to determine which ItemIdentifier will be equipped. The current slot cannot be used from the inventory because not all // items may have been equipped yet. var itemIdentifier = itemSetManager.GetEquipItemIdentifier(i); if (itemIdentifier == null) { continue; } // Skip if the item is null or matches the current item. The current item can't give ammo to itself. var item = m_Inventory.GetItem(itemIdentifier, i); if (item == null || item == m_Item) { continue; } // Find any ShootableWeapons that may be sharing the same Consumable ItemIdentifier. var itemActions = item.ItemActions; for (int j = 0; j < itemActions.Length; ++j) { var shootableWeapon = itemActions[j] as ShootableWeapon; // The ShootableWeapon has to share the Consumable ItemIdentifier. The other shootable weapon must have ammo in the clip otherwise it isn't of any use. if (shootableWeapon == null || shootableWeapon.GetConsumableItemIdentifier() != m_ConsumableItemIdentifier || shootableWeapon.ClipRemaining == 0) { continue; } // The Consumable ItemIdentifier doesn't need to be shared if there is plenty of ammo for all weapons. var totalInventoryAmount = m_Inventory.GetItemIdentifierAmount(m_ConsumableItemIdentifier); if (shootableWeapon.ClipSize + m_ClipSize < totalInventoryAmount) { continue; } // The ShootableWeapon needs to share the ammo. Take half of the ammo and return it to the inventory. When the current item is reloaded it will // then take the Consumable ItemIdentifier that was returned to the inventory. var totalConsumable = shootableWeapon.ClipRemaining + m_ClipRemaining + totalInventoryAmount; var returnConsumable = Mathf.FloorToInt(shootableWeapon.ClipRemaining - (totalConsumable - (totalConsumable / 2))); if (returnConsumable > 0) { shootableWeapon.ClipRemaining -= returnConsumable; m_Inventory.Pickup(m_ConsumableItemIdentifier, returnConsumable, -1, false, false, false); returnedConsumable = true; } } } } if (CanReloadItem(false)) { // If part of the consumable was returned then the weapon should take as much as it can from the remaining amount. var activeCount = m_ActiveSharedConsumableItemIdentifierAmount; if (returnedConsumable) { m_ActiveSharedConsumableItemIdentifierAmount--; } ReloadItem(true); m_ActiveSharedConsumableItemIdentifierAmount = activeCount; } m_ReloadInitialized = true; } } /// /// The item has been equipped by the character. /// public override void Equip() { base.Equip(); DetermineVisibleProjectile(false); DetermineVisibleScopeCamera(); m_CanPlayDryFireAnimation = ClipRemaining == 0; EventHandler.RegisterEvent(m_Character, "OnItemStartUse", ItemStartUse); EventHandler.RegisterEvent(m_Character, "OnAnimatorItemReloadDetachClip", DetachClip); EventHandler.RegisterEvent(m_Character, "OnAnimatorItemReloadAttachClip", AttachClip); EventHandler.RegisterEvent(m_Character, "OnAnimatorItemReloadDropClip", DropClip); EventHandler.RegisterEvent(m_Character, "OnAnimatorItemReloadShowProjectile", ShowReloadProjectile); EventHandler.RegisterEvent(m_Character, "OnAnimatorItemReloadAttachProjectile", AttachReloadProjectile); } /// /// Returns the ItemIdentifier which can be used by the item. /// /// The ItemIdentifier which can be used by the item. public override IItemIdentifier GetConsumableItemIdentifier() { return m_ConsumableItemIdentifier; } /// /// Sets the UsableItemIdentifier amount on the UsableItem. /// /// The amount to set the UsableItemIdentifier to. public override void SetConsumableItemIdentifierAmount(int amount) { ClipRemaining = amount; } /// /// Returns the amout of UsableItemIdentifier which has been consumed by the UsableItem. /// /// The amount consumed of the UsableItemIdentifier. public override int GetConsumableItemIdentifierAmount() { return ClipRemaining; } /// /// Removes the amout of UsableItemIdentifier which has been loaded by the UsableItem. /// public override void RemoveConsumableItemIdentifierAmount() { ClipRemaining = 0; } /// /// Returns the substate index that the item should be in. /// /// the substate index that the item should be in. public override int GetItemSubstateIndex() { // A positive value should always be returned when the item is being fired. if (m_Firing) { if (m_ConsumableItemIdentifier != null && m_CanPlayDryFireAnimation) { return m_DryFireItemSubstateParameterValue; } if (m_FireType == FireType.Instant) { return Mathf.Max(1, base.GetItemSubstateIndex()); } else { if (m_StartChargeTime != -1) { if (m_WaitForCharge) { return m_ChargeItemSubstateParameterValue; } return Mathf.Max(1, base.GetItemSubstateIndex()); } } } return -1; } /// /// Can the item be used? /// /// The itemAbility that is trying to use the item. /// The state of the Use ability when calling CanUseItem. /// True if the item can be used. public override bool CanUseItem(ItemAbility itemAbility, UseAbilityState abilityState) { if (!base.CanUseItem(itemAbility, abilityState)) { return false; } // The weapon cannot be used if the fire mode prevents it. if (!m_FireModeCanUse) { return false; } // The weapon can't be used if it is reloading and out of ammo. if (m_Reloading && (m_ReloadType == ReloadClipType.Full || m_ClipRemaining == 0)) { return false; } // The weapon properties can prevent the weapon from firing. if (!m_ShootableWeaponPerspectiveProperties.CanFire(abilityState == UseAbilityState.Update, m_FireInLookSourceDirection && !m_CharacterLocomotion.ActiveMovementType.UseIndependentLook(true))) { return false; } // The weapon can't be used if it is already being used. if (abilityState == UseAbilityState.Start && m_Firing) { return false; } // The weapon can be used. return true; } /// /// Starts the item use. /// /// The item ability that is using the item. public override void StartItemUse(ItemAbility itemAbility) { base.StartItemUse(itemAbility); // An Animator Audio State Set may prevent the item from being used. if (!IsItemInUse()) { return; } m_FacingTarget = false; m_UseCount = 0; m_TryStopUse = false; m_Charged = false; m_WaitForCharge = false; m_StartChargeTime = -1; DetermineVisibleProjectile(false); BurstReset(); m_CanPlayDryFireAnimation = ClipRemaining == 0; m_CanPlayDryFireAudio = true; } /// /// Allows the item to update while it is being used. /// public override void UseItemUpdate() { base.UseItemUpdate(); if (m_FacingTarget) { return; } // When the weapon is being used it will rotate to face the look source direction. Wait to fire until the weapon is looking at the look source. if (m_ShootableWeaponPerspectiveProperties.CanFire(true, m_FireInLookSourceDirection && !m_CharacterLocomotion.ActiveMovementType.UseIndependentLook(true))) { m_FacingTarget = true; m_Firing = true; m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); } } /// /// Uses the item. /// public override void UseItem() { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif // The weapon may not have any more ammo left. Play a dry fire sound and return immediately so the weapon is not fired. if (m_ConsumableItemIdentifier != null && ClipRemaining == 0) { if (!m_Reloading && m_FireModeCanUse && m_CanPlayDryFireAudio) { // If it wasn't for being out of ammo the item would have been able to be used - play a dry fire sound. m_DryFireAudioClipSet.PlayAudioClip(m_Item.GetVisibleObject()); m_CanPlayDryFireAnimation = true; m_CanPlayDryFireAudio = false; } m_NextAllowedUseTime = Time.time + m_UseRate; m_FireModeCanUse = m_FireMode == FireMode.FullAuto; return; } if (m_FireType == FireType.Instant) { Fire(1); } else if (!m_WaitForCharge) { // The FireType requires charging. m_StartChargeTime = Time.time; m_Charged = false; m_WaitForCharge = true; m_ChargeAudioClipSet.PlayAudioClip(m_Item.GetVisibleObject(), 0); m_ChargeEvent = Scheduler.ScheduleFixed(m_FullChargeLength, OnCharged); m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); } } /// /// Is the item waiting to be used? This will return true if the item is waiting to be charged or pulled back. /// /// Returns true if the item is waiting to be used. public override bool IsItemUsePending() { if (m_FireType == FireType.Instant || !m_WaitForCharge) { return false; } // The item is waiting to be charged. return true; } /// /// The weapon has finished charging. /// private void OnCharged() { m_Charged = true; m_ChargeEvent = null; if (m_FireType == FireType.ChargeAndFire) { m_WaitForCharge = false; m_ChargeAudioClipSet.Stop(m_Item.GetVisibleObject(), 0); Fire(1); } } /// /// Fires the weapon. /// /// (0 - 1) value indicating the amount of strength to apply to the shot. public virtual void Fire(float strength) { // Call base.UseItem within Fire since this is where the weapon use actually occurs. base.UseItem(); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { if (m_NetworkCharacter != null) { m_NetworkCharacter.Fire(this, strength); } #endif m_Firing = true; m_UseCount++; if (m_FireMode == FireMode.Burst) { // The weapon can continue to be fired if the current burst count is greater than the total burst count. m_FireModeCanUse = m_UseCount < m_BurstCount; // If the weapon can't be used that means the burst count needs to be reset after the specified delay. if (!m_FireModeCanUse) { m_BurstEvent = Scheduler.ScheduleFixed(m_BurstDelay, BurstReset); } } else { // Prevent the weapon from continuously firing if it not a fully automatic. m_FireModeCanUse = m_FireMode == FireMode.FullAuto; } m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER } #endif // Update the clips remaining after the parameters so dry fire doesn't play when there was still a single shot left. if (m_ConsumableItemIdentifier != null) { ClipRemaining--; } // Fire as many projectiles or hitscan bullets as the fire count specifies. for (int i = 0; i < m_FireCount; ++i) { if (m_Projectile) { ProjectileFire(strength); } else { Scheduler.Schedule(m_HitscanFireDelay, HitscanFire, strength); } } // The weapon has been fired - add any effects. ApplyFireEffects(); } /// /// Spawns a projectile which will move in the firing direction. /// /// (0 - 1) value indicating the amount of strength to apply to the shot. private void ProjectileFire(float strength) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // Do not spawn the projectile unless on the server. The server will manage the projectile. if (m_NetworkInfo != null && !m_NetworkInfo.IsServer()) { if (m_SpawnedProjectile != null) { ObjectPool.Destroy(m_SpawnedProjectile); m_SpawnedProjectile = null; } return; } #endif var firePoint = m_ShootableWeaponPerspectiveProperties.FirePointLocation.position; var fireDirection = FireDirection(firePoint); var rotation = Quaternion.LookRotation(fireDirection); // The projectile will already be spawned if it is always visible. if (m_SpawnedProjectile == null) { m_SpawnedProjectile = ObjectPool.Instantiate(m_Projectile, firePoint, rotation * m_Projectile.transform.rotation); } else { // The projectile may be on the other side of an object (especially in the case of separate arms for the first person perspective). Perform a linecast // to ensure the projectile doesn't go through any objects. if (m_FireInLookSourceDirection && !m_CharacterLocomotion.ActiveMovementType.UseIndependentLook(false) && Physics.Linecast(m_LookSource.LookPosition(), m_SpawnedProjectile.transform.position, out m_RaycastHit, m_ImpactLayers, QueryTriggerInteraction.Ignore)) { // The cast should not hit the character that it belongs to. var updatePosition = true; var hitGameObject = m_RaycastHit.transform.gameObject; var parentCharacterLocomotion = hitGameObject.GetCachedParentComponent(); if (parentCharacterLocomotion != null && parentCharacterLocomotion == m_CharacterLocomotion) { updatePosition = false; } #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. if (updatePosition && hitGameObject.GetCachedParentComponent() != null) { updatePosition = false; } #endif if (updatePosition) { m_SpawnedProjectile.transform.position = m_RaycastHit.point; } EventHandler.ExecuteEvent(m_Character, "OnShootableWeaponShowProjectile", m_SpawnedProjectile, false); } m_SpawnedProjectile.transform.parent = null; } // Optionally change the layer after the object has been fired. This allows the object to change from the first person Overlay layer // to the Default layer after it has cleared the first person weapon. if (m_ProjectileStartLayer != m_ProjectileFiredLayer) { Scheduler.ScheduleFixed(m_LayerChangeDelay, ChangeFiredLayer, m_SpawnedProjectile); } var projectile = m_SpawnedProjectile.GetCachedComponent(); projectile.Initialize(rotation * Vector3.forward * m_ProjectileFireVelocityMagnitude * strength, Vector3.zero, m_DamageAmount, m_ImpactForce, m_ImpactForceFrames, m_ImpactLayers, m_ImpactStateName, m_ImpactStateDisableTimer, m_SurfaceImpact, m_Character); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkCharacter != null) { NetworkObjectPool.NetworkSpawn(m_Projectile, projectile.gameObject, true); } #endif m_SpawnedProjectile = null; } /// /// Changes the fired projectile to the fired layer. /// /// The projectile that was fired. private void ChangeFiredLayer(GameObject projectileObject) { projectileObject.transform.SetLayerRecursively(m_ProjectileFiredLayer); } /// /// Enables or disables the visibile projectile. /// /// Should the projectile be enabled? private void DetermineVisibleProjectile(bool forceDisable) { if (m_Projectile == null || m_ProjectileVisibility == ProjectileVisiblityType.OnFire) { return; } // The projectile should be visible if: // - The item isn't being unequipped and is active. // - The projectile is always visible. // - The projectile is visible upon aim and the aim ability is active. // - The projectile is visible when reloading and the character is reloading. var enable = !forceDisable && m_Item.IsActive() && ((m_ProjectileVisibility == ProjectileVisiblityType.Always) || (m_Aiming && m_ProjectileVisibility == ProjectileVisiblityType.OnAim) || ((m_ShowReloadProjectile != ShowProjectileStatus.NotShown) && m_ProjectileVisibility == ProjectileVisiblityType.OnReload)); // The projectile can't be shown if there are no projectiles remaining. if (enable && m_ClipRemaining == 0 && m_ShowReloadProjectile == ShowProjectileStatus.NotShown) { return; } if (enable) { if (m_SpawnedProjectile == null) { m_SpawnedProjectile = ObjectPool.Instantiate(m_Projectile); m_SpawnedProjectile.transform.SetLayerRecursively(m_ProjectileStartLayer); // If the character is reloading the projectile should be shown if (m_ShowReloadProjectile == ShowProjectileStatus.AttachmentLocation) { m_SpawnedProjectile.transform.SetParentOrigin(m_ShootableWeaponPerspectiveProperties.ReloadProjectileAttachment); m_SpawnedProjectile.layer = m_ShootableWeaponPerspectiveProperties.ReloadProjectileAttachment.gameObject.layer; } else { m_SpawnedProjectile.transform.SetParentOrigin(m_ShootableWeaponPerspectiveProperties.FirePointLocation); } } else if (m_ShowReloadProjectile == ShowProjectileStatus.FirePointLocation) { m_SpawnedProjectile.transform.parent = m_ShootableWeaponPerspectiveProperties.FirePointLocation; } EventHandler.ExecuteEvent(m_Character, "OnShootableWeaponShowProjectile", m_SpawnedProjectile, true); } else if (m_SpawnedProjectile != null) { EventHandler.ExecuteEvent(m_Character, "OnShootableWeaponShowProjectile", m_SpawnedProjectile, false); ObjectPool.Destroy(m_SpawnedProjectile); m_SpawnedProjectile = null; } } /// /// Determines if the scope camera should be visible. /// private void DetermineVisibleScopeCamera() { if (m_ShootableWeaponPerspectiveProperties != null && m_ShootableWeaponPerspectiveProperties.ScopeCamera != null) { m_ShootableWeaponPerspectiveProperties.ScopeCamera.SetActive(!m_DisableScopeCameraOnNoAim || m_Aiming); } } /// /// The Aim ability has started or stopped. /// /// Has the Aim ability started? /// Was the ability started from input? private void OnAim(bool aim, bool inputStart) { if (!inputStart) { return; } m_Aiming = aim; DetermineVisibleProjectile(false); DetermineVisibleScopeCamera(); } /// /// Fire by casting a ray in the specified direction. If an object was hit then apply damage, apply a force, add a decal, etc. /// /// (0 - 1) value indicating the amount of strength to apply to the shot. private void HitscanFire(float strength) { // The hitscan should be fired from the center of the camera so the hitscan will always hit the correct crosshairs location. var useLookPosition = m_FireInLookSourceDirection && !m_CharacterLocomotion.ActiveMovementType.UseIndependentLook(false); var firePoint = useLookPosition ? m_LookSource.LookPosition() : m_ShootableWeaponPerspectiveProperties.FirePointLocation.position; var fireDirection = FireDirection(firePoint); var fireRay = new Ray(firePoint, fireDirection); // Prevent the ray between the character and the look source from causing a false collision. if (useLookPosition && !m_CharacterLocomotion.FirstPersonPerspective) { var direction = m_CharacterTransform.InverseTransformPoint(firePoint); direction.y = 0; fireRay.origin = fireRay.GetPoint(direction.magnitude); } var hitCount = Physics.RaycastNonAlloc(fireRay, m_HitscanRaycastHits, m_HitscanFireRange * strength, m_ImpactLayers.value, m_HitscanTriggerInteraction); var hasHit = false; #if UNITY_EDITOR if (hitCount == m_MaxHitscanCollisionCount) { Debug.LogWarning($"Warning: The maximum number of colliders have been hit by {m_GameObject.name}. Consider increasing the Max Hitscan Collision Count value."); } #endif for (int i = 0; i < hitCount; ++i) { var closestRaycastHit = QuickSelect.SmallestK(m_HitscanRaycastHits, hitCount, i, m_RaycastHitComparer); var hitGameObject = closestRaycastHit.transform.gameObject; // The character can't shoot themself. if (hitGameObject.transform.IsChildOf(m_CharacterTransform) #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. || hitGameObject.GetCachedParentComponent() != null #endif ) { continue; } // The shield can absorb some (or none) of the damage from the hitscan. var damageAmount = m_DamageAmount; #if ULTIMATE_CHARACTER_CONTROLLER_MELEE ShieldCollider shieldCollider; if ((shieldCollider = hitGameObject.GetCachedComponent()) != null) { damageAmount = shieldCollider.Shield.Damage(this, damageAmount); } #endif // Allow a custom event to be received. EventHandler.ExecuteEvent(closestRaycastHit.transform.gameObject, "OnObjectImpact", damageAmount, closestRaycastHit.point, fireDirection * m_ImpactForce * strength, m_Character, this, closestRaycastHit.collider); if (m_OnHitscanImpactEvent != null) { m_OnHitscanImpactEvent.Invoke(damageAmount, closestRaycastHit.point, fireDirection * m_ImpactForce * strength, m_Character); } // If the shield didn't absorb all of the damage then it should be applied to the character. if (damageAmount > 0) { // If the Health component exists it will apply a force to the rigidbody in addition to deducting the health. Otherwise just apply the force to the rigidbody. Health hitHealth; if ((hitHealth = hitGameObject.GetCachedParentComponent()) != null) { hitHealth.Damage(damageAmount, closestRaycastHit.point, fireDirection, m_ImpactForce * strength, m_ImpactForceFrames, 0, m_Character, this, closestRaycastHit.collider); } else if (m_ImpactForce > 0 && closestRaycastHit.rigidbody != null && !closestRaycastHit.rigidbody.isKinematic) { closestRaycastHit.rigidbody.AddForceAtPosition(fireDirection * m_ImpactForce * strength * MathUtility.RigidbodyForceMultiplier, closestRaycastHit.point); } } // Spawn a tracer which moves to the hit point. if (m_Tracer != null) { Scheduler.ScheduleFixed(m_TracerSpawnDelay, AddHitscanTracer, closestRaycastHit.point); } // An optional state can be activated on the hit object. if (!string.IsNullOrEmpty(m_ImpactStateName)) { StateManager.SetState(hitGameObject, m_ImpactStateName, true); // If the timer isn't -1 then the state should be disabled after a specified amount of time. If it is -1 then the state // will have to be disabled manually. if (m_ImpactStateDisableTimer != -1) { StateManager.DeactivateStateTimer(hitGameObject, m_ImpactStateName, m_ImpactStateDisableTimer); } } // The surface manager will apply effects based on the type of bullet hit. SurfaceManager.SpawnEffect(closestRaycastHit, m_SurfaceImpact, m_CharacterLocomotion.GravityDirection, m_CharacterLocomotion.TimeScale, m_Item.GetVisibleObject()); hasHit = true; break; } // A tracer should still be spawned if no object was hit. if (!hasHit && m_Tracer != null) { Scheduler.ScheduleFixed(m_TracerSpawnDelay, AddHitscanTracer, MathUtility.TransformPoint(firePoint, Quaternion.LookRotation(fireDirection), new Vector3(0, 0, m_TracerDefaultLength))); } } /// /// Adds a tracer to the hitscan weapon. /// /// The position that the tracer should move towards. protected virtual void AddHitscanTracer(Vector3 position) { var tracerLocation = m_ShootableWeaponPerspectiveProperties.TracerLocation; var tracerObject = ObjectPool.Instantiate(m_Tracer, tracerLocation.position, tracerLocation.rotation); var tracer = tracerObject.GetCachedComponent(); if (tracer != null) { tracer.Initialize(position); } } /// /// Determines the direction to fire. /// /// The direction to fire. private Vector3 FireDirection(Vector3 firePoint) { var direction = (m_FireInLookSourceDirection ? m_LookSource.LookDirection(firePoint, false, m_ImpactLayers, true) : m_ShootableWeaponPerspectiveProperties.FirePointLocation.forward); // Add a random spread. if (m_Spread > 0) { direction += Quaternion.AngleAxis(UnityEngine.Random.Range(0, 360), direction) * m_CharacterLocomotion.Up * UnityEngine.Random.Range(0, m_Spread / 360); } return direction; } /// /// Adds any effects (muzzle flash, shell, recoil, etc) to the fire position. /// protected virtual void ApplyFireEffects() { // Spawn a muzzle flash. if (m_MuzzleFlash != null) { SpawnMuzzleFlash(); } // Spawn a shell. if (m_Shell != null) { Scheduler.Schedule(m_ShellEjectDelay, EjectShell); } // Spawn the smoke. if (m_Smoke != null) { Scheduler.Schedule(m_SmokeSpawnDelay, SpawnSmoke); } // Apply the weapon recoil. EventHandler.ExecuteEvent(m_Character, "OnAddSecondaryForce", m_Item.SlotID, m_PositionRecoil.RandomValue, m_RotationRecoil.RandomValue, !m_LocalizeRecoilForce); EventHandler.ExecuteEvent(m_Character, "OnAddSecondaryCameraForce", m_PositionCameraRecoil.RandomValue, m_RotationCameraRecoil.RandomValue, m_CameraRecoilAccumulation); EventHandler.ExecuteEvent(m_Character, "OnAddCrosshairsSpread", true, true); } /// /// Spawns the muzzle flash. /// private void SpawnMuzzleFlash() { if (m_PoolMuzzleFlash || m_SpawnedMuzzleFlash == null) { m_SpawnedMuzzleFlash = ObjectPool.Instantiate(m_MuzzleFlash, Vector3.zero, Quaternion.identity); } var muzzleFlashLocation = m_ShootableWeaponPerspectiveProperties.MuzzleFlashLocation; m_SpawnedMuzzleFlash.transform.parent = muzzleFlashLocation; m_SpawnedMuzzleFlash.transform.localScale = m_MuzzleFlash.transform.localScale; m_SpawnedMuzzleFlash.transform.position = muzzleFlashLocation.position + m_MuzzleFlash.transform.position; // Choose a random z rotation angle. var eulerAngles = m_MuzzleFlash.transform.eulerAngles; eulerAngles.z = UnityEngine.Random.Range(0, 360); m_SpawnedMuzzleFlash.transform.localRotation = Quaternion.Euler(eulerAngles); var muzzleFlashObj = m_SpawnedMuzzleFlash.GetCachedComponent(); if (muzzleFlashObj != null) { muzzleFlashObj.Show(m_Item, m_ID, m_PoolMuzzleFlash, m_CharacterLocomotion); } } /// /// Ejects the shell. /// private void EjectShell() { var shellLocation = m_ShootableWeaponPerspectiveProperties.ShellLocation; var shell = ObjectPool.Instantiate(m_Shell, shellLocation.position, shellLocation.rotation); var shellObj = shell.GetCachedComponent(); if (shellObj != null) { var visibleObject = m_Item.ActivePerspectiveItem.GetVisibleObject(); shellObj.Initialize(visibleObject.transform.TransformDirection(m_ShellVelocity.RandomValue), m_ShellTorque.RandomValue, m_Character); } } /// /// Spawns the shell. /// private void SpawnSmoke() { var smokeLocation = m_ShootableWeaponPerspectiveProperties.SmokeLocation; var smoke = ObjectPool.Instantiate(m_Smoke, smokeLocation.position, smokeLocation.rotation); var smokeObj = smoke.GetCachedComponent(); if (smokeObj != null) { smokeObj.Show(m_Item, m_ID, m_CharacterLocomotion); } } /// /// Resets the use count which will allow burst to start again. /// private void BurstReset() { m_BurstEvent = null; if (!m_TryStopUse) { m_UseCount = 0; m_FireModeCanUse = true; } } /// /// The item has been used. /// public override void ItemUseComplete() { base.ItemUseComplete(); m_Firing = false; // When the clip is empty the weapon should be reloaded if specified. if (ClipRemaining == 0 && (m_AutoReload & Reload.AutoReloadType.Empty) != 0) { EventHandler.ExecuteEvent(m_Character, "OnItemTryReload", m_Item.SlotID, m_ConsumableItemIdentifier, false, false); } } /// /// Tries to stop the item use. The weapon may be fired here if the FireType is not instant. /// public override void TryStopItemUse() { base.TryStopItemUse(); m_TryStopUse = true; if (m_FireType != FireType.Instant && m_FireModeCanUse) { m_ChargeAudioClipSet.Stop(m_Item.GetVisibleObject(), 0); m_WaitForCharge = false; if (!m_Charged) { // If the weapon has been stopped before it has been fully charged then the item should be fired with a reduced velocity. if (m_StartChargeTime != -1 && Time.time > m_StartChargeTime + m_MinChargeLength) { var strength = Mathf.Clamp01((Time.time - m_StartChargeTime) / (m_FullChargeLength - m_MinChargeLength)); strength = m_MinChargeStrength + (1 - m_MinChargeStrength) * strength; Fire(strength); } else { m_StartChargeTime = -1; m_CharacterLocomotion.UpdateItemAbilityAnimatorParameters(); } Scheduler.Cancel(m_ChargeEvent); m_ChargeEvent = null; } else if (m_FireType == FireType.ChargeAndHold) { Fire(1); } } } /// /// Can the item use be stopped? /// /// True if the item use can be stopped. public override bool CanStopItemUse() { // An instant fire type must fire at least once before it can be stopped. if (m_FireType == FireType.Instant && (m_Firing || !m_FacingTarget)) { return false; } // The full burst amount should complete before the item is stopped. if (m_FireMode == FireMode.Burst && m_UseCount < m_BurstCount && m_Inventory.GetItemIdentifierAmount(m_ConsumableItemIdentifier) > 0) { return false; } return base.CanStopItemUse(); } /// /// Stops the item use. /// public override void StopItemUse() { base.StopItemUse(); m_FireModeCanUse = true; m_Firing = false; if (m_BurstEvent != null) { Scheduler.Cancel(m_BurstEvent); m_BurstEvent = null; } } /// /// Returns the ItemIdentifier which can be reloaded by the item. /// /// The ItemIdentifier which can be reloaded by the item. public IItemIdentifier GetReloadableItemIdentifier() { return m_ConsumableItemIdentifier; } /// /// Can the item be reloaded? /// /// Should extra checks be performed ensuring the reload is valid for shared items? /// True if the item can be reloaded. public bool CanReloadItem(bool sharedCheck) { // Don't reload if the clip size is infinitely large or at capacity. if (m_ClipSize == int.MaxValue || ClipRemaining == m_ClipSize) { return false; } // Don't reload if the inventory is out of ammo. if (m_Inventory.GetItemIdentifierAmount(m_ConsumableItemIdentifier) == 0) { return false; } // Don't reload if the consumed item type is shared and the item isn't equipped. This will prevent an unequipped shootable weapon from taking ammo. if (sharedCheck && m_SharedConsumableItemIdentifier && m_Inventory.GetActiveItem(m_Item.SlotID) != m_Item) { return false; } // Can't reload if the weapon hasn't been added to the inventory yet. if (m_Inventory.GetItemIdentifierAmount(m_Item.ItemIdentifier) == 0) { return false; } return true; } /// /// Starts to reload the item. /// public void StartItemReload() { m_Reloading = true; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { if (m_NetworkCharacter != null) { m_NetworkCharacter.StartItemReload(this); } } #endif if (m_ActiveSharedConsumableItemIdentifierAmount > 0) { DetermineTotalReloadAmount(); } var detachClip = false; IShootableWeaponPerspectiveProperties shootableWeaponPerspectiveProperties; #if FIRST_PERSON_CONTROLLER shootableWeaponPerspectiveProperties = m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties; if (shootableWeaponPerspectiveProperties != null) { detachClip = shootableWeaponPerspectiveProperties.ReloadableClip != null; } #endif if (!detachClip) { shootableWeaponPerspectiveProperties = m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties; if (shootableWeaponPerspectiveProperties != null) { detachClip = shootableWeaponPerspectiveProperties.ReloadableClip != null; } } if (detachClip) { if (!m_ReloadDetachClipEvent.WaitForAnimationEvent) { m_ReloadDetachAttachClipEvent = Scheduler.ScheduleFixed(m_ReloadDetachClipEvent.Duration, DetachClip); } } // The reload AnimatorAudioState is starting. m_ReloadAnimatorAudioStateSet.StartStopStateSelection(true); m_ReloadAnimatorAudioStateSet.NextState(); // Optionally play a reload sound based upon the reload animation. if (m_ReloadType == ReloadClipType.Full) { m_ReloadAnimatorAudioStateSet.PlayAudioClip(m_Item.GetVisibleObject()); } // The crosshairs should be set to the max spread. if (m_ReloadCrosshairsSpread) { EventHandler.ExecuteEvent(m_Character, "OnAddCrosshairsSpread", true, false); } // The projectile may become visible when the item is reloaded. if (m_ProjectileVisibility == ProjectileVisiblityType.OnReload) { if (!m_ReloadShowProjectileEvent.WaitForAnimationEvent) { m_ReloadProjectileEvent = Scheduler.ScheduleFixed(m_ReloadShowProjectileEvent.Duration, ShowReloadProjectile); } } } /// /// Determines the total amount of the consumable ItemIdentifier that needs to be reloaded. /// This method only needs to be used if there are multiple active shared consumable ItemIdentifiers. /// private void DetermineTotalReloadAmount() { m_TotalReloadAmount = m_ClipSize - ClipRemaining; m_TotalClipAmount = ClipRemaining; if (m_ActiveSharedConsumableItemIdentifierAmount > 0) { for (int i = 0; i < m_Inventory.SlotCount; ++i) { var item = m_Inventory.GetActiveItem(i); if (item != null) { var shootableWeapons = item.gameObject.GetCachedComponents(); for (int j = 0; j < shootableWeapons.Length; ++j) { if (shootableWeapons[j] == this) { continue; } m_TotalReloadAmount += shootableWeapons[j].ClipSize - shootableWeapons[j].ClipRemaining; m_TotalClipAmount += shootableWeapons[j].ClipRemaining; } } } } m_TotalConsumableAmount = m_Inventory.GetItemIdentifierAmount(m_ConsumableItemIdentifier); } /// /// The clip has been detached from the weapon. /// private void DetachClip() { if (!m_Reloading) { return; } // Attach the clip to the attachment transform. Attach both first and third person in case there is a perspective switch. #if FIRST_PERSON_CONTROLLER DetachAttachClip(true, m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, true); #endif DetachAttachClip(true, m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, false); // Clean up from the detach event. if (m_ReloadDetachAttachClipEvent != null) { Scheduler.Cancel(m_ReloadDetachAttachClipEvent); m_ReloadDetachAttachClipEvent = null; } // The clip can actually be dropped. if (m_ReloadDropClip != null) { if (!m_ReloadDropClipEvent.WaitForAnimationEvent) { m_ReloadClipEvent = Scheduler.ScheduleFixed(m_ReloadDropClipEvent.Duration, DropClip); } } // Schedule the event which will attach clip back to the weapon. if (!m_ReloadAttachClipEvent.WaitForAnimationEvent) { m_ReloadDetachAttachClipEvent = Scheduler.ScheduleFixed(m_ReloadAttachClipEvent.Duration, AttachClip); } } /// /// Detaches or attaches the clip from the weapon. /// /// Should the clip be detached? If false the clip will be attached. /// A reference to the perspective's IShootableWeaponPerspectiveProperties. /// Is the first person perspective being affected? private void DetachAttachClip(bool detach, IShootableWeaponPerspectiveProperties shootableWeaponPerspectiveProperties, bool firstPerson) { // If the perspective properties is null then that perspective isn't setup for the character. if (shootableWeaponPerspectiveProperties == null) { return; } // Don't do anything if the clip doesn't exist or the clip shouldn't be detached. var clip = shootableWeaponPerspectiveProperties.ReloadableClip; if (clip == null || (!m_ReloadDetachAttachClip && detach)) { return; } // If detaching then set the clip's parent from the weapon to the attachment object. Attaching will set the clip's parent from the attachment // object back to the weapon. if (detach) { if (firstPerson) { if (m_FirstPersonReloadableClipParent == null) { m_FirstPersonReloadableClipParent = clip.parent; m_FirstPersonReloadableClipLocalPosition = clip.localPosition; m_FirstPersonReloadableClipLocalRotation = clip.localRotation; } } else { if (m_ThirdPersonReloadableClipParent == null) { m_ThirdPersonReloadableClipParent = clip.parent; m_ThirdPersonReloadableClipLocalPosition = clip.localPosition; m_ThirdPersonReloadableClipLocalRotation = clip.localRotation; } } clip.parent = shootableWeaponPerspectiveProperties.ReloadableClipAttachment; } else { if (firstPerson) { if (m_FirstPersonReloadableClipParent != null) { clip.parent = m_FirstPersonReloadableClipParent; clip.localPosition = m_FirstPersonReloadableClipLocalPosition; clip.localRotation = m_FirstPersonReloadableClipLocalRotation; m_FirstPersonReloadableClipParent = null; } } else { if (m_ThirdPersonReloadableClipParent != null) { clip.parent = m_ThirdPersonReloadableClipParent; clip.localPosition = m_ThirdPersonReloadableClipLocalPosition; clip.localRotation = m_ThirdPersonReloadableClipLocalRotation; m_ThirdPersonReloadableClipParent = null; } } } } /// /// Drops the weapon's clip. /// private void DropClip() { if (!m_Reloading || m_ReloadDropClip == null) { return; } // Hide the existing clip and drop a new prefab. var clip = (m_ActivePerspectiveProperties as IShootableWeaponPerspectiveProperties).ReloadableClip; if (clip == null) { return; } if (m_ReloadDetachAttachClip) { clip.gameObject.SetActive(false); } var dropClip = ObjectPool.Instantiate(m_ReloadDropClip, clip.position, clip.rotation); // The first person perspective requires the clip to be on the overlay layer so the fingers won't render in front of the clip. dropClip.transform.SetLayerRecursively(m_CharacterLocomotion.FirstPersonPerspective ? LayerManager.Overlay : clip.gameObject.layer); Scheduler.Schedule(m_ReloadClipLayerChangeDelay, UpdateDropClipLayer, dropClip); // If the clip has a trajectory object attached then it needs to be initialized. var trajectoryClipObject = dropClip.GetCachedComponent(); if (trajectoryClipObject != null) { trajectoryClipObject.Initialize(Vector3.zero, Vector3.zero, m_Character); } // Cleanup from the event. if (m_ReloadClipEvent != null) { Scheduler.Cancel(m_ReloadClipEvent); m_ReloadClipEvent = null; } } /// /// Updates the dropped clip layer. /// /// The object that should have its layer updated. private void UpdateDropClipLayer(GameObject dropClip) { dropClip.transform.SetLayerRecursively(m_ReloadClipTargetLayer); } /// /// The clip has been detached form the weapon. /// private void AttachClip() { if (!m_Reloading) { return; } // Attach the clip back to the original transform. Attach both first and third person in case there is a perspective switch. #if FIRST_PERSON_CONTROLLER DetachAttachClip(false, m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, true); AddRemoveReloadableClip(false, m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, true); #endif DetachAttachClip(false, m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, false); AddRemoveReloadableClip(false, m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, false); // Clean up from the event. if (m_ReloadDetachAttachClipEvent != null) { Scheduler.Cancel(m_ReloadDetachAttachClipEvent); m_ReloadDetachAttachClipEvent = null; } } /// /// Shows the reload projectile at the attachment location. /// private void ShowReloadProjectile() { m_ShowReloadProjectile = ShowProjectileStatus.AttachmentLocation; DetermineVisibleProjectile(false); // The projectile will be attached to the fire point in the future. if (!ReloadAttachProjectileEvent.WaitForAnimationEvent) { m_ReloadProjectileEvent = Scheduler.ScheduleFixed(ReloadAttachProjectileEvent.Duration, AttachReloadProjectile); } } /// /// Attaches the reload projectile to the fire point. /// private void AttachReloadProjectile() { m_ShowReloadProjectile = ShowProjectileStatus.FirePointLocation; DetermineVisibleProjectile(false); } /// /// Reloads the item. /// /// Should the full clip be force reloaded? public void ReloadItem(bool fullClip) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { if (m_NetworkCharacter != null) { m_NetworkCharacter.ReloadItem(this, fullClip); } } #endif if (!fullClip) { EventHandler.ExecuteEvent(m_Character, "OnAddCrosshairsSpread", false, false); } var consumableItemIdentifierAmount = m_Inventory.GetItemIdentifierAmount(m_ConsumableItemIdentifier); if (consumableItemIdentifierAmount == 0) { return; } int reloadAmount; if (m_ActiveSharedConsumableItemIdentifierAmount > 0) { if (!m_Reloading) { DetermineTotalReloadAmount(); } if (m_TotalReloadAmount > m_TotalConsumableAmount) { var totalAmount = m_TotalConsumableAmount + m_TotalClipAmount; // If there are multiple active consumable ItemIdentifiers then the reloaded count is shared across all of the ItemIdentifiers. var targetAmount = (fullClip || m_ReloadType == ReloadClipType.Full) ? Mathf.CeilToInt(totalAmount / (float)(m_ActiveSharedConsumableItemIdentifierAmount + 1)) : ClipRemaining + 1; reloadAmount = Mathf.Min(consumableItemIdentifierAmount, Mathf.Min(m_ClipSize - ClipRemaining, targetAmount - ClipRemaining)); } else { // The Consumable ItemIdentifier doesn't need to be shared if there is plenty of ammo for all weapons. reloadAmount = Mathf.Min(m_TotalConsumableAmount, ((fullClip || m_ReloadType == ReloadClipType.Full) ? (m_ClipSize - ClipRemaining) : 1)); } } else { // The consumable ItemIdentifier doesn't share with any other objects. reloadAmount = Mathf.Min(consumableItemIdentifierAmount, ((fullClip || m_ReloadType == ReloadClipType.Full) ? (m_ClipSize - ClipRemaining) : 1)); } ClipRemaining += reloadAmount; m_UseCount = 0; m_Inventory.AdjustItemIdentifierAmount(m_ConsumableItemIdentifier, -reloadAmount); if (!fullClip && m_ReloadType == ReloadClipType.Single) { m_ReloadAnimatorAudioStateSet.PlayAudioClip(m_Item.GetVisibleObject()); // If the item cannot be reloaded any more then the complete animation will play. if (CanReloadItem(false)) { m_ReloadAnimatorAudioStateSet.NextState(); } } if (!fullClip && m_ReloadDropClip != null) { // When the item is reloaded the clip should also be replaced. #if FIRST_PERSON_CONTROLLER AddRemoveReloadableClip(true, m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, true); #endif AddRemoveReloadableClip(true, m_ThirdPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties, false); } if (m_ShowReloadProjectile != ShowProjectileStatus.NotShown) { m_ShowReloadProjectile = ShowProjectileStatus.NotShown; DetermineVisibleProjectile(false); } } /// /// Adds or removes the instantiated reloadable clip. /// /// Should the reloadable clip be instantiated? /// A reference to the perspective's IShootableWeaponPerspectiveProperties. /// Is the first person perspective being affected? private void AddRemoveReloadableClip(bool add, IShootableWeaponPerspectiveProperties shootableWeaponPerspectiveProperties, bool firstPerson) { // If the perspective properties is null then that perspective isn't setup for the character. if (shootableWeaponPerspectiveProperties == null) { return; } // If the clip can't be detached then the weapon's clip shouldn't be disabled. if (!m_ReloadDetachAttachClip) { return; } if (add) { var clip = ObjectPool.Instantiate(m_ReloadDropClip, shootableWeaponPerspectiveProperties.ReloadableClip.position, shootableWeaponPerspectiveProperties.ReloadableClip.rotation); var remover = clip.GetCachedComponent(); if (remover != null) { remover.CancelRemoveEvent(); } // The first person perspective requires the clip to be on the overlay layer so the fingers won't render in front of the clip. clip.transform.SetLayerRecursively(firstPerson ? LayerManager.Overlay : clip.gameObject.layer); clip.transform.SetParentOrigin(shootableWeaponPerspectiveProperties.ReloadableClipAttachment); clip.transform.position = shootableWeaponPerspectiveProperties.ReloadableClip.position; clip.transform.rotation = shootableWeaponPerspectiveProperties.ReloadableClip.rotation; if (firstPerson) { m_FirstPersonReloadAddClip = clip; m_FirstPersonReloadAddClip.SetActive(m_CharacterLocomotion.FirstPersonPerspective); } else { m_ThirdPersonReloadAddClip = clip; m_ThirdPersonReloadAddClip.SetActive(!m_CharacterLocomotion.FirstPersonPerspective); } } else { var clip = firstPerson ? m_FirstPersonReloadAddClip : m_ThirdPersonReloadAddClip; if (clip != null) { ObjectPool.Destroy(clip); clip = null; if (firstPerson) { m_FirstPersonReloadAddClip = null; } else { m_ThirdPersonReloadAddClip = null; } } shootableWeaponPerspectiveProperties.ReloadableClip.gameObject.SetActive(true); } } /// /// The item has finished reloading. /// /// Was the item reloaded successfully? /// Should the item be reloaded immediately? public void ItemReloadComplete(bool success, bool immediateReload) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { if (m_NetworkCharacter != null) { m_NetworkCharacter.ItemReloadComplete(this, success, immediateReload); } } #endif if (!success) { // The weapon will not be successfully reloaded if the Reload ability stopped early. AttachClip(); if (m_ReloadClipEvent != null) { Scheduler.Cancel(m_ReloadClipEvent); m_ReloadClipEvent = null; } if (m_ReloadProjectileEvent != null) { Scheduler.Cancel(m_ReloadProjectileEvent); m_ReloadProjectileEvent = null; } m_ShowReloadProjectile = ShowProjectileStatus.NotShown; } else if (!immediateReload) { m_ReloadCompleteAudioClipSet.PlayAudioClip(m_Item.GetVisibleObject()); } DetermineVisibleProjectile(false); m_Reloading = false; // The item has been reloaded - inform the state set. m_ReloadAnimatorAudioStateSet.StartStopStateSelection(false); m_CanPlayDryFireAudio = true; } /// /// An item has started or stopped being used. If the projectile is visible then it may need to be disabled. /// /// The item that is being used. /// True if the item is starting to be used. private void ItemStartUse(IUsableItem usableItem, bool start) { if ((usableItem as ItemAction) == this) { return; } if (!start) { Scheduler.ScheduleFixed(m_ProjectileEnableDelayAfterOtherUse, DetermineVisibleProjectile, start); } else { DetermineVisibleProjectile(start); } } /// /// The camera perspective between first and third person has changed. /// /// Is the character in a first person perspective? protected override void OnChangePerspectives(bool firstPersonPerspective) { base.OnChangePerspectives(firstPersonPerspective); if (m_ActivePerspectiveProperties != null) { m_ShootableWeaponPerspectiveProperties = m_ActivePerspectiveProperties as IShootableWeaponPerspectiveProperties; // The FirePointLocation cannot be null. if (m_ShootableWeaponPerspectiveProperties.FirePointLocation == null) { Debug.LogError($"Error: The FirePointLocation is null on the ShootableWeaponPerspectiveProperties of the {name}."); } DetermineVisibleScopeCamera(); } if (m_SpawnedProjectile != null) { if (m_ShowReloadProjectile != ShowProjectileStatus.NotShown) { if (m_ShowReloadProjectile == ShowProjectileStatus.AttachmentLocation) { m_SpawnedProjectile.transform.SetParentOrigin(m_ShootableWeaponPerspectiveProperties.ReloadProjectileAttachment); m_SpawnedProjectile.layer = m_ShootableWeaponPerspectiveProperties.ReloadProjectileAttachment.gameObject.layer; } else { // Keep the projectile in the same relative location. var localPosition = m_SpawnedProjectile.transform.localPosition; var localRotation = m_SpawnedProjectile.transform.localRotation; m_SpawnedProjectile.transform.parent = m_ShootableWeaponPerspectiveProperties.FirePointLocation; m_SpawnedProjectile.transform.localPosition = localPosition; m_SpawnedProjectile.transform.localRotation = localRotation; } } else { m_SpawnedProjectile.transform.SetParentOrigin(m_ShootableWeaponPerspectiveProperties.FirePointLocation); } } // If the clip has been dropped then the opposite perspective add clip should be disabled. if (m_FirstPersonReloadAddClip != null && m_ThirdPersonReloadAddClip != null) { m_FirstPersonReloadAddClip.SetActive(firstPersonPerspective); m_ThirdPersonReloadAddClip.SetActive(!firstPersonPerspective); } if (!firstPersonPerspective) { // The scope camera exists doesn't need to render while in third person view. if (m_FirstPersonPerspectiveProperties != null) { var firstPersonShootableWeaponPerspectiveProperties = m_FirstPersonPerspectiveProperties as IShootableWeaponPerspectiveProperties; if (firstPersonShootableWeaponPerspectiveProperties.ScopeCamera != null) { firstPersonShootableWeaponPerspectiveProperties.ScopeCamera.SetActive(false); } } } } /// /// The item has started to be unequipped by the character. /// public override void StartUnequip() { base.StartUnequip(); if (!m_Aiming) { DetermineVisibleProjectile(true); } } /// /// The item has been unequipped by the character. /// public override void Unequip() { base.Unequip(); DetermineVisibleProjectile(true); EventHandler.UnregisterEvent(m_Character, "OnItemStartUse", ItemStartUse); EventHandler.UnregisterEvent(m_Character, "OnAnimatorItemReloadDetachClip", DetachClip); EventHandler.UnregisterEvent(m_Character, "OnAnimatorItemReloadAttachClip", AttachClip); EventHandler.UnregisterEvent(m_Character, "OnAnimatorItemReloadDropClip", DropClip); EventHandler.UnregisterEvent(m_Character, "OnAnimatorItemReloadShowProjectile", ShowReloadProjectile); EventHandler.UnregisterEvent(m_Character, "OnAnimatorItemReloadAttachProjectile", AttachReloadProjectile); } /// /// The inventory has added the specified item. /// /// The item that was added. private void OnAddItem(Item item) { CheckForSharedConsumableItemIdentifiers(item.GetComponents()); } /// /// Determines if the ShootableWeapon is sharing the consumable ItemIdentifier with another ShootableWeapon. /// /// The array of ShootableWeapons which have been added to the character. private void CheckForSharedConsumableItemIdentifiers(ShootableWeapon[] shootableWeapons) { if (m_ConsumableItemIdentifier == null) { return; } for (int i = 0; i < shootableWeapons.Length; ++i) { if (shootableWeapons[i] == this) { continue; } // Increase the count if the ItemIdentifiers match. The current ShootableWeapon is sharing ItemIdentifiers. if (shootableWeapons[i].GetConsumableItemIdentifier() == m_ConsumableItemIdentifier) { if (!m_SharedConsumableItemIdentifier) { m_SharedConsumableItemIdentifier = true; m_SharedConsumableItemIdentifierAmountMap = new Dictionary(); } var count = 0; m_SharedConsumableItemIdentifierAmountMap.TryGetValue(shootableWeapons[i].Item, out count); count++; m_SharedConsumableItemIdentifierAmountMap[shootableWeapons[i].Item] = count; } } } /// /// An item has been equipped. /// /// The equipped item. /// The slot that the item now occupies. private void OnEquipItem(Item item, int slotID) { if (!m_SharedConsumableItemIdentifier) { return; } int amount; m_SharedConsumableItemIdentifierAmountMap.TryGetValue(item, out amount); m_ActiveSharedConsumableItemIdentifierAmount += amount; } /// /// An item has been unequipped. /// /// The item that was unequipped. /// The slot that the item was unequipped from. private void OnUnequipItem(Item item, int slotID) { if (!m_SharedConsumableItemIdentifier) { return; } int amount; m_SharedConsumableItemIdentifierAmountMap.TryGetValue(item, out amount); m_ActiveSharedConsumableItemIdentifierAmount -= amount; } /// /// The item has been removed by the character. /// public override void Remove() { m_ReloadInitialized = false; } /// /// Callback when the StateManager has changed the active state on the current object. /// public override void StateChange() { base.StateChange(); if (m_ActivePerspectiveProperties != null) { DetermineVisibleProjectile(false); } } /// /// The GameObject has been destroyed. /// protected override void OnDestroy() { base.OnDestroy(); m_ReloadAnimatorAudioStateSet.OnDestroy(); EventHandler.UnregisterEvent(m_Character, "OnAimAbilityStart", OnAim); EventHandler.UnregisterEvent(m_Character, "OnInventoryAddItem", OnAddItem); EventHandler.UnregisterEvent(m_Character, "OnInventoryEquipItem", OnEquipItem); EventHandler.UnregisterEvent(m_Character, "OnInventoryUnequipItem", OnUnequipItem); } } }