using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; namespace BNG { /// /// An example weapon script that can fire Raycasts or Projectile objects /// public class RaycastWeapon : GrabbableEvents { [Header("General : ")] /// /// How far we can shoot in meters /// public float MaxRange = 25f; /// /// How much damage to apply to "Damageable" on contact /// public float Damage = 25f; /// /// Semi requires user to press trigger repeatedly, Auto to hold down /// [Tooltip("Semi requires user to press trigger repeatedly, Auto to hold down")] public FiringType FiringMethod = FiringType.Semi; /// /// How does the user reload once the Clip is Empty /// public ReloadType ReloadMethod = ReloadType.InfiniteAmmo; /// /// Ex : 0.2 = 5 Shots per second /// [Tooltip("Ex : 0.2 = 5 Shots per second")] public float FiringRate = 0.2f; float lastShotTime; [Tooltip("Amount of force to apply to a Rigidbody once damaged")] public float BulletImpactForce = 1000f; /// /// Maximum amount of internal ammo this weapon can hold. Does not account for attached clips. For example, a shotgun has internal ammo /// [Tooltip("Current Internal Ammo if you are keeping track of ammo yourself. Firing will deduct from this number. Reloading will cause this to equal MaxInternalAmmo.")] public float InternalAmmo = 0; /// /// Maximum amount of internal ammo this weapon can hold. Does not account for attached clips. For example, a shotgun has internal ammo /// [Tooltip("Maximum amount of internal ammo this weapon can hold. Does not account for attached clips. For example, a shotgun has internal ammo")] public float MaxInternalAmmo = 10; /// /// Set true to automatically chamber a new round on fire. False to require charging. Example : Bolt-Action Rifle does not auto chamber. /// [Tooltip("Set true to automatically chamber a new round on fire. False to require charging. Example : Bolt-Action Rifle does not auto chamber. ")] public bool AutoChamberRounds = true; /// /// Does it matter if rounds are chambered or not. Does the user have to charge weapon as soon as ammo is inserted /// [Tooltip("Does it matter if rounds are chambered or not. Does the user have to charge weapon as soon as ammo is inserted")] public bool MustChamberRounds = false; [Header("Projectile Settings : ")] [Tooltip("If true a projectile will always be used instead of a raycast")] public bool AlwaysFireProjectile = false; [Tooltip("If true the ProjectilePrefab will be instantiated during slowmo instead of using a raycast.")] public bool FireProjectileInSlowMo = true; [Tooltip("How fast to fire the weapon during slowmo. Keep in mind this is affected by Time.timeScale")] public float SlowMoRateOfFire = 0.3f; [Tooltip("Amount of force to apply to Projectile")] public float ShotForce = 10f; [Tooltip("Amount of force to apply to the BulletCasingPrefab object")] public float BulletCasingForce = 3f; [Header("Recoil : ")] /// /// How much force to apply to the tip of the barrel /// [Tooltip("How much force to apply to the tip of the barrel")] public Vector3 RecoilForce = Vector3.zero; [Tooltip("Time in seconds to allow the gun to be springy")] public float RecoilDuration = 0.3f; Rigidbody weaponRigid; [Header("Raycast Options : ")] public LayerMask ValidLayers; [Header("Weapon Setup : ")] /// /// Transform of trigger to animate rotation of /// [Tooltip("Transform of trigger to animate rotation of")] public Transform TriggerTransform; /// /// Move this back on fire /// [Tooltip("Animate this back on fire")] public Transform SlideTransform; /// /// Where our raycast or projectile will spawn from /// [Tooltip("Where our raycast or projectile will start from.")] public Transform MuzzlePointTransform; /// /// Where to eject a bullet casing (optional) /// [Tooltip("Where to eject a bullet casing (optional)")] public Transform EjectPointTransform; /// /// Transform of Chambered Bullet. Hide this when no bullet is chambered /// [Tooltip("Transform of Chambered Bullet inside the weapon. Hide this when no bullet is chambered. (Optional)")] public Transform ChamberedBullet; /// /// Make this active on fire. Randomize scale / rotation /// [Tooltip("Make this active on fire. Randomize scale / rotation")] public GameObject MuzzleFlashObject; /// /// Eject this at EjectPointTransform (optional) /// [Tooltip("Eject this at EjectPointTransform (optional)")] public GameObject BulletCasingPrefab; /// /// If time is slowed this object will be instantiated instead of using a raycast /// [Tooltip("If time is slowed this object will be instantiated at muzzle point instead of using a raycast")] public GameObject ProjectilePrefab; /// /// Hit Effects spawned at point of impact /// [Tooltip("Hit Effects spawned at point of impact")] public GameObject HitFXPrefab; /// /// Play this sound on shoot /// [Tooltip("Play this sound on shoot")] public AudioClip GunShotSound; [Tooltip("Volume to play the GunShotSound clip at. Range 0-1")] [Range(0.0f, 1f)] public float GunShotVolume = 0.75f; /// /// Play this sound if no ammo and user presses trigger /// [Tooltip("Play this sound if no ammo and user presses trigger")] public AudioClip EmptySound; [Tooltip("Volume to play the EmptySound clip at. Range 0-1")] [Range(0.0f, 1f)] public float EmptySoundVolume = 1f; [Header("Slide Configuration : ")] /// /// How far back to move the slide on fire /// [Tooltip("How far back to move the slide on fire")] public float SlideDistance = -0.028f; /// /// Should the slide be forced back if we shoot the last bullet /// [Tooltip("Should the slide be forced back if we shoot the last bullet")] public bool ForceSlideBackOnLastShot = true; [Tooltip("How fast to move back the slide on fire. Default : 1")] public float slideSpeed = 1; /// /// How close to the origin is considered valid. /// float minSlideDistance = 0.001f; [Header("Inputs : ")] [Tooltip("Controller Input used to eject clip")] public List EjectInput = new List() { GrabbedControllerBinding.Button2Down }; [Tooltip("Controller Input used to release the charging mechanism.")] public List ReleaseSlideInput = new List() { GrabbedControllerBinding.Button1Down }; [Tooltip("Controller Input used to release reload the weapon if ReloadMethod = InternalAmmo.")] public List ReloadInput = new List() { GrabbedControllerBinding.Button2Down }; [Header("Shown for Debug : ")] /// /// Is there currently a bullet chambered and ready to be fired /// [Tooltip("Is there currently a bullet chambered and ready to be fired")] public bool BulletInChamber = false; /// /// Is there currently a bullet chambered and that must be ejected /// [Tooltip("Is there currently a bullet chambered and that must be ejected")] public bool EmptyBulletInChamber = false; [Header("Events")] [Tooltip("Unity Event called when Shoot() method is successfully called")] public UnityEvent onShootEvent; [Tooltip("Unity Event called when something attaches ammo to the weapon")] public UnityEvent onAttachedAmmoEvent; [Tooltip("Unity Event called when something detaches ammo from the weapon")] public UnityEvent onDetachedAmmoEvent; [Tooltip("Unity Event called when the charging handle is successfully pulled back on the weapon")] public UnityEvent onWeaponChargedEvent; [Tooltip("Unity Event called when weapon damaged something")] public FloatEvent onDealtDamageEvent; [Tooltip("Passes along Raycast Hit info whenever a Raycast hit is successfully detected. Use this to display fx, add force, etc.")] public RaycastHitEvent onRaycastHitEvent; /// /// Is the slide / receiver forced back due to last shot /// protected bool slideForcedBack = false; protected WeaponSlide ws; protected bool readyToShoot = true; void Start() { weaponRigid = GetComponent(); if (MuzzleFlashObject) { MuzzleFlashObject.SetActive(false); } ws = GetComponentInChildren(); updateChamberedBullet(); } public override void OnTrigger(float triggerValue) { // Sanitize for angles triggerValue = Mathf.Clamp01(triggerValue); // Update trigger graphics if (TriggerTransform) { TriggerTransform.localEulerAngles = new Vector3(triggerValue * 15, 0, 0); } // Trigger up, reset values if (triggerValue <= 0.5) { readyToShoot = true; playedEmptySound = false; } // Fire gun if possible if (readyToShoot && triggerValue >= 0.75f) { Shoot(); // Immediately ready to keep firing if readyToShoot = FiringMethod == FiringType.Automatic; } // These are here for convenience. Could be called through GrabbableUnityEvents instead checkSlideInput(); checkEjectInput(); CheckReloadInput(); updateChamberedBullet(); base.OnTrigger(triggerValue); } void checkSlideInput() { // Check for bound controller button to release the charging mechanism for (int x = 0; x < ReleaseSlideInput.Count; x++) { if (InputBridge.Instance.GetGrabbedControllerBinding(ReleaseSlideInput[x], thisGrabber.HandSide)) { UnlockSlide(); break; } } } void checkEjectInput() { // Check for bound controller button to eject magazine for (int x = 0; x < EjectInput.Count; x++) { if (InputBridge.Instance.GetGrabbedControllerBinding(EjectInput[x], thisGrabber.HandSide)) { EjectMagazine(); break; } } } public virtual void CheckReloadInput() { if(ReloadMethod == ReloadType.InternalAmmo) { // Check for Reload input(s) for (int x = 0; x < ReloadInput.Count; x++) { if (InputBridge.Instance.GetGrabbedControllerBinding(EjectInput[x], thisGrabber.HandSide)) { Reload(); break; } } } } public virtual void UnlockSlide() { if (ws != null) { ws.UnlockBack(); } } public virtual void EjectMagazine() { MagazineSlide ms = GetComponentInChildren(); if (ms != null) { ms.EjectMagazine(); } } protected bool playedEmptySound = false; public virtual void Shoot() { // Has enough time passed between shots float shotInterval = Time.timeScale < 1 ? SlowMoRateOfFire : FiringRate; if (Time.time - lastShotTime < shotInterval) { return; } // Need to Chamber round into weapon if(!BulletInChamber && MustChamberRounds) { // Only play empty sound once per trigger down if(!playedEmptySound) { VRUtils.Instance.PlaySpatialClipAt(EmptySound, transform.position, EmptySoundVolume, 0.5f); playedEmptySound = true; } return; } // Need to release slide if(ws != null && ws.LockedBack) { VRUtils.Instance.PlaySpatialClipAt(EmptySound, transform.position, EmptySoundVolume, 0.5f); return; } // Create our own spatial clip VRUtils.Instance.PlaySpatialClipAt(GunShotSound, transform.position, GunShotVolume); // Haptics if (thisGrabber != null) { input.VibrateController(0.1f, 0.2f, 0.1f, thisGrabber.HandSide); } // Use projectile if Time has been slowed bool useProjectile = AlwaysFireProjectile || (FireProjectileInSlowMo && Time.timeScale < 1); if (useProjectile) { GameObject projectile = Instantiate(ProjectilePrefab, MuzzlePointTransform.position, MuzzlePointTransform.rotation) as GameObject; Rigidbody projectileRigid = projectile.GetComponentInChildren(); projectileRigid.AddForce(MuzzlePointTransform.forward * ShotForce, ForceMode.VelocityChange); Projectile proj = projectile.GetComponent(); // Convert back to raycast if Time reverts if (proj && !AlwaysFireProjectile) { proj.MarkAsRaycastBullet(); } // Make sure we clean up this projectile Destroy(projectile, 20); } else { // Raycast to hit RaycastHit hit; if (Physics.Raycast(MuzzlePointTransform.position, MuzzlePointTransform.forward, out hit, MaxRange, ValidLayers, QueryTriggerInteraction.Ignore)) { OnRaycastHit(hit); } } // Apply recoil ApplyRecoil(); // We just fired this bullet BulletInChamber = false; // Try to load a new bullet into chamber if (AutoChamberRounds) { chamberRound(); } else { EmptyBulletInChamber = true; } // Unable to chamber bullet, force slide back if(!BulletInChamber) { // Do we need to force back the receiver? slideForcedBack = ForceSlideBackOnLastShot; if (slideForcedBack && ws != null) { ws.LockBack(); } } // Call Shoot Event if(onShootEvent != null) { onShootEvent.Invoke(); } // Store our last shot time to be used for rate of fire lastShotTime = Time.time; // Stop previous routine if (shotRoutine != null) { MuzzleFlashObject.SetActive(false); StopCoroutine(shotRoutine); } if (AutoChamberRounds) { shotRoutine = animateSlideAndEject(); StartCoroutine(shotRoutine); } else { shotRoutine = doMuzzleFlash(); StartCoroutine(shotRoutine); } } // Apply recoil by requesting sprinyness and apply a local force to the muzzle point public virtual void ApplyRecoil() { if (weaponRigid != null && RecoilForce != Vector3.zero) { // Make weapon springy for X seconds grab.RequestSpringTime(RecoilDuration); // Apply the Recoil Force weaponRigid.AddForceAtPosition(MuzzlePointTransform.TransformDirection(RecoilForce), MuzzlePointTransform.position, ForceMode.VelocityChange); } } // Hit something without Raycast. Apply damage, apply FX, etc. public virtual void OnRaycastHit(RaycastHit hit) { ApplyParticleFX(hit.point, Quaternion.FromToRotation(Vector3.forward, hit.normal), hit.collider); // push object if rigidbody Rigidbody hitRigid = hit.collider.attachedRigidbody; if (hitRigid != null) { hitRigid.AddForceAtPosition(BulletImpactForce * MuzzlePointTransform.forward, hit.point); } // Damage if possible Damageable d = hit.collider.GetComponent(); if (d) { d.DealDamage(Damage, hit.point, hit.normal, true, gameObject, hit.collider.gameObject); if (onDealtDamageEvent != null) { onDealtDamageEvent.Invoke(Damage); } } // Call event if (onRaycastHitEvent != null) { onRaycastHitEvent.Invoke(hit); } } public virtual void ApplyParticleFX(Vector3 position, Quaternion rotation, Collider attachTo) { if(HitFXPrefab) { GameObject impact = Instantiate(HitFXPrefab, position, rotation) as GameObject; // Attach bullet hole to object if possible BulletHole hole = impact.GetComponent(); if (hole) { hole.TryAttachTo(attachTo); } } } /// /// Something attached ammo to us /// public virtual void OnAttachedAmmo() { // May have ammo loaded updateChamberedBullet(); if(onAttachedAmmoEvent != null) { onAttachedAmmoEvent.Invoke(); } } // Ammo was detached from the weapon public virtual void OnDetachedAmmo() { // May have ammo loaded / unloaded updateChamberedBullet(); if (onDetachedAmmoEvent != null) { onDetachedAmmoEvent.Invoke(); } } public virtual int GetBulletCount() { if (ReloadMethod == ReloadType.InfiniteAmmo) { return 9999; } else if (ReloadMethod == ReloadType.InternalAmmo) { return (int)InternalAmmo; } else if (ReloadMethod == ReloadType.ManualClip) { return GetComponentsInChildren(false).Length; } // Default to bullet count return GetComponentsInChildren(false).Length; } public virtual void RemoveBullet() { // Don't remove bullet here if (ReloadMethod == ReloadType.InfiniteAmmo) { return; } else if (ReloadMethod == ReloadType.InternalAmmo) { InternalAmmo--; } else if (ReloadMethod == ReloadType.ManualClip) { Bullet firstB = GetComponentInChildren(false); // Deactivate gameobject as this bullet has been consumed if (firstB != null) { Destroy(firstB.gameObject); } } // Whenever we remove a bullet is a good time to check the chamber updateChamberedBullet(); } public virtual void Reload() { InternalAmmo = MaxInternalAmmo; } void updateChamberedBullet() { if (ChamberedBullet != null) { ChamberedBullet.gameObject.SetActive(BulletInChamber || EmptyBulletInChamber); } } void chamberRound() { int currentBulletCount = GetBulletCount(); if(currentBulletCount > 0) { // Remove the first bullet we find in the clip RemoveBullet(); // That bullet is now in chamber BulletInChamber = true; } // Unable to chamber a bullet else { BulletInChamber = false; } } protected IEnumerator shotRoutine; // Randomly scale / rotate to make them seem different void randomizeMuzzleFlashScaleRotation() { MuzzleFlashObject.transform.localScale = Vector3.one * Random.Range(0.75f, 1.5f); MuzzleFlashObject.transform.localEulerAngles = new Vector3(0, 0, Random.Range(0, 90f)); } public virtual void OnWeaponCharged(bool allowCasingEject) { // Already bullet in chamber, eject it if (BulletInChamber && allowCasingEject) { ejectCasing(); } else if (EmptyBulletInChamber && allowCasingEject) { ejectCasing(); EmptyBulletInChamber = false; } chamberRound(); // Slide is no longer forced back if weapon was just charged slideForcedBack = false; if(onWeaponChargedEvent != null) { onWeaponChargedEvent.Invoke(); } } protected virtual void ejectCasing() { GameObject shell = Instantiate(BulletCasingPrefab, EjectPointTransform.position, EjectPointTransform.rotation) as GameObject; Rigidbody rb = shell.GetComponentInChildren(); if (rb) { rb.AddRelativeForce(Vector3.right * BulletCasingForce, ForceMode.VelocityChange); } // Clean up shells GameObject.Destroy(shell, 5); } protected virtual IEnumerator doMuzzleFlash() { MuzzleFlashObject.SetActive(true); yield return new WaitForSeconds(0.05f); randomizeMuzzleFlashScaleRotation(); yield return new WaitForSeconds(0.05f); MuzzleFlashObject.SetActive(false); } // Animate the slide back, eject casing, pull slide back protected virtual IEnumerator animateSlideAndEject() { // Start Muzzle Flash MuzzleFlashObject.SetActive(true); int frames = 0; bool slideEndReached = false; Vector3 slideDestination = new Vector3(0, 0, SlideDistance); if(SlideTransform) { while (!slideEndReached) { SlideTransform.localPosition = Vector3.MoveTowards(SlideTransform.localPosition, slideDestination, Time.deltaTime * slideSpeed); float distance = Vector3.Distance(SlideTransform.localPosition, slideDestination); if (distance <= minSlideDistance) { slideEndReached = true; } frames++; // Go ahead and update muzzleflash in sync with slide if (frames < 2) { randomizeMuzzleFlashScaleRotation(); } else { slideEndReached = true; MuzzleFlashObject.SetActive(false); } yield return new WaitForEndOfFrame(); } } else { yield return new WaitForEndOfFrame(); randomizeMuzzleFlashScaleRotation(); yield return new WaitForEndOfFrame(); MuzzleFlashObject.SetActive(false); slideEndReached = true; } // Set Slide Position if(SlideTransform) { SlideTransform.localPosition = slideDestination; } yield return new WaitForEndOfFrame(); MuzzleFlashObject.SetActive(false); // Eject Shell ejectCasing(); // Pause for shell to eject before returning slide yield return new WaitForEndOfFrame(); if(!slideForcedBack && SlideTransform != null) { // Slide back to original position frames = 0; bool slideBeginningReached = false; while (!slideBeginningReached) { SlideTransform.localPosition = Vector3.MoveTowards(SlideTransform.localPosition, Vector3.zero, Time.deltaTime * slideSpeed); float distance = Vector3.Distance(SlideTransform.localPosition, Vector3.zero); if (distance <= minSlideDistance) { slideBeginningReached = true; } if (frames > 2) { slideBeginningReached = true; } yield return new WaitForEndOfFrame(); } } } } public enum FiringType { Semi, Automatic } public enum ReloadType { InfiniteAmmo, ManualClip, InternalAmmo } }