// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Devices;
using UltimateXR.Manipulation;
using UnityEngine;
namespace UltimateXR.Mechanics.Weapons
{
///
/// Type of weapon that shoots projectiles. A firearm has one or more entries. Each
/// trigger allows to shoot a different type of projectile, and determines properties such as the shot cycle, shot
/// frequency, ammunition, recoil and grabbing.
/// A requires a component that defines the
/// projectiles being shot. If a firearm has more than one trigger (for instance, a rifle that shoots bullets and has a
/// grenade launcher), the will require the same amount of entries in
/// .
///
[RequireComponent(typeof(UxrProjectileSource))]
public partial class UxrFirearmWeapon : UxrWeapon
{
#region Inspector Properties/Serialized Fields
[SerializeField] protected Transform _recoilAxes;
[SerializeField] private List _triggers;
#endregion
#region Public Types & Data
///
/// Event called right after the weapon shot a projectile using the given trigger index.
///
public event Action ProjectileShot;
#endregion
#region Public Methods
///
/// Checks whether a trigger is in a loaded state, meaning it is ready to shoot if pressed and there is any ammo left.
///
/// Index in
/// Whether it is ready to shoot
public bool IsLoaded(int triggerIndex)
{
if (_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return runtimeTrigger.HasReloaded;
}
return false;
}
///
/// Sets the given weapon trigger loaded state so that it is ready to shoot if there is ammo left.
///
/// Index in
public void Reload(int triggerIndex)
{
if (!_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return;
}
BeginSync();
runtimeTrigger.HasReloaded = true;
EndSyncMethod(new object[] { triggerIndex });
}
///
/// Checks whether there is a magazine attached that fires shots using the given trigger. It may or may not have ammo.
///
/// Index in
/// Whether there is a magazine attached
public bool HasMagAttached(int triggerIndex)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return false;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
return trigger.AmmunitionMagAnchor != null && trigger.AmmunitionMagAnchor.CurrentPlacedObject != null;
}
///
/// Gets the attached magazine maximum capacity.
///
/// Index in
/// Maximum capacity of ammo in the attached magazine. If there isn't any magazine attached it returns 0
public int GetAmmoCapacity(int triggerIndex)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return 0;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
if (trigger.AmmunitionMagAnchor != null && trigger.AmmunitionMagAnchor.CurrentPlacedObject != null)
{
UxrFirearmMag mag = trigger.AmmunitionMagAnchor.CurrentPlacedObject.GetCachedComponent();
return mag != null ? mag.Capacity : 0;
}
return 0;
}
///
/// Gets the ammo left in the attached magazine.
///
/// Index in
/// Ammo left in the attached magazine. If there isn't any magazine attached it returns 0
public int GetAmmoLeft(int triggerIndex)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return 0;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
if (trigger.AmmunitionMagAnchor != null)
{
if (trigger.AmmunitionMagAnchor.CurrentPlacedObject != null)
{
UxrFirearmMag mag = trigger.AmmunitionMagAnchor.CurrentPlacedObject.GetCachedComponent();
return mag != null ? mag.Rounds : 0;
}
}
else
{
return int.MaxValue;
}
return 0;
}
///
/// Sets the ammo left in the attached magazine.
///
/// Index in
/// New ammo
public void SetAmmoLeft(int triggerIndex, int ammo)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
if (trigger.AmmunitionMagAnchor != null && trigger.AmmunitionMagAnchor.CurrentPlacedObject != null)
{
UxrFirearmMag mag = trigger.AmmunitionMagAnchor.CurrentPlacedObject.GetCachedComponent();
if (mag != null)
{
mag.Rounds = ammo;
}
}
}
///
/// Sets the trigger pressed amount.
///
/// Index in
/// Pressed amount between range [0.0, 1.0]
public void SetTriggerPressedAmount(int triggerIndex, float amount)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return;
}
if (!_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
if (trigger.TriggerTransform)
{
trigger.TriggerTransform.localRotation = runtimeTrigger.TriggerInitialLocalRotation * Quaternion.AngleAxis(trigger.TriggerRotationDegrees * amount, trigger.TriggerRotationAxis);
}
}
///
/// Tries to shoot a round using the given trigger.
///
/// Index in
///
/// Whether a round was shot. If no round was shot it can mean that:
///
/// - The trigger index references an entry that doesn't exist.
/// - The firearm isn't loaded.
/// - The firearm doesn't have any ammo left or there is no magazine attached.
/// - The shoot frequency doesn't allow to shoot again so quickly.
///
///
public bool TryToShootRound(int triggerIndex)
{
if (triggerIndex < 0 || triggerIndex >= _triggers.Count)
{
return false;
}
if (!_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return false;
}
UxrFirearmTrigger trigger = _triggers[triggerIndex];
if (GetAmmoLeft(triggerIndex) > 0 && runtimeTrigger.LastShotTimer <= 0.0f)
{
SetAmmoLeft(triggerIndex, GetAmmoLeft(triggerIndex) - 1);
runtimeTrigger.LastShotTimer = trigger.MaxShotFrequency > 0 ? 1.0f / trigger.MaxShotFrequency : -1.0f;
// TODO: here we probably should add some randomization depending on recoil using the additional optional parameters
_weaponSource.Shoot(trigger.ProjectileShotIndex);
runtimeTrigger.RecoilTimer = trigger.RecoilDurationSeconds;
// Audio
trigger.ShotAudio?.Play(_weaponSource.GetShotOrigin(trigger.ProjectileShotIndex));
// Raise events
OnProjectileShot(triggerIndex);
return true;
}
return false;
}
#endregion
#region Unity
///
protected override void Awake()
{
base.Awake();
for (int i = 0; i < _triggers.Count; ++i)
{
if (!_runtimeTriggers.ContainsKey(i))
{
_runtimeTriggers.Add(i, new RuntimeTriggerInfo());
}
}
}
///
/// Subscribes to events.
///
protected override void OnEnable()
{
base.OnEnable();
if (RootGrabbable)
{
RootGrabbable.ConstraintsApplied += RootGrabbable_ConstraintsApplied;
}
UxrManager.AvatarsUpdated += UxrManager_AvatarsUpdated;
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (trigger.TriggerGrabbable != null)
{
trigger.TriggerGrabbable.Released += Trigger_Released;
trigger.TriggerGrabbable.Placed += Trigger_Placed;
}
if (trigger.AmmunitionMagAnchor != null)
{
trigger.AmmunitionMagAnchor.Placed += MagTarget_Placed;
trigger.AmmunitionMagAnchor.Removed += MagTarget_Removed;
}
}
}
///
/// Unsubscribes from events.
///
protected override void OnDisable()
{
base.OnDisable();
if (RootGrabbable)
{
RootGrabbable.ConstraintsApplied -= RootGrabbable_ConstraintsApplied;
}
UxrManager.AvatarsUpdated -= UxrManager_AvatarsUpdated;
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (trigger.TriggerGrabbable != null)
{
trigger.TriggerGrabbable.Released -= Trigger_Released;
trigger.TriggerGrabbable.Placed -= Trigger_Placed;
}
if (trigger.AmmunitionMagAnchor != null)
{
trigger.AmmunitionMagAnchor.Placed -= MagTarget_Placed;
trigger.AmmunitionMagAnchor.Removed -= MagTarget_Removed;
}
}
}
///
/// Initializes the component.
///
protected override void Start()
{
base.Start();
_weaponSource = GetCachedComponent();
for (int i = 0; i < _triggers.Count; i++)
{
UxrFirearmTrigger trigger = _triggers[i];
if (_runtimeTriggers.TryGetValue(i, out RuntimeTriggerInfo info))
{
info.LastShotTimer = -1.0f;
info.HasReloaded = true;
if (trigger.TriggerTransform)
{
info.TriggerInitialLocalRotation = trigger.TriggerTransform.localRotation;
}
}
if (trigger.AmmunitionMagAnchor != null)
{
if (trigger.AmmunitionMagAnchor.CurrentPlacedObject != null)
{
// Disable mag collider while it is attached
Collider magCollider = trigger.AmmunitionMagAnchor.CurrentPlacedObject.GetComponentInChildren();
if (magCollider != null)
{
magCollider.enabled = false;
}
}
}
}
}
#endregion
#region Event Handling Methods
///
/// Called when the grip of a grabbable object for a given trigger was released.
///
/// Sender
/// Event parameters
private void Trigger_Released(object sender, UxrManipulationEventArgs e)
{
if (e.Grabber != null && e.Grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
SyncAmmoLeft(e.GrabbableObject);
}
}
///
/// Called when the grip of a grabbable object for a given trigger was placed.
///
/// Sender
/// Event parameters
private void Trigger_Placed(object sender, UxrManipulationEventArgs e)
{
if (e.Grabber != null && e.Grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
SyncAmmoLeft(e.GrabbableObject);
}
}
///
/// Called after the avatars have been updated. Updates the hand trigger blend value.
///
private void UxrManager_AvatarsUpdated()
{
for (int i = 0; i < _triggers.Count; i++)
{
UxrFirearmTrigger trigger = _triggers[i];
if (!_runtimeTriggers.TryGetValue(i, out RuntimeTriggerInfo runtimeTrigger))
{
continue;
}
// Check if we are grabbing the given grip using the local avatar
if (trigger.TriggerGrabbable && UxrGrabManager.Instance.GetGrabbingHand(trigger.TriggerGrabbable, trigger.GrabbableGrabPointIndex, out UxrGrabber grabber))
{
if (grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
// Get the trigger press amount and use it to send it to the animation var that controls the hand trigger. Use it to rotate the trigger as well.
float triggerPressAmount = UxrAvatar.LocalAvatarInput.GetInput1D(grabber.Side, UxrInput1D.Trigger);
trigger.TriggerGrabbable.GetGrabPoint(trigger.GrabbableGrabPointIndex).GetGripPoseInfo(grabber.Avatar).PoseBlendValue = triggerPressAmount;
SetTriggerPressedAmount(i, triggerPressAmount);
// Now depending on the weapon type check if we need to shoot
SyncTriggerPressStates(i,
UxrAvatar.LocalAvatarInput.GetButtonsPress(grabber.Side, UxrInputButtons.Trigger),
UxrAvatar.LocalAvatarInput.GetButtonsPressDown(grabber.Side, UxrInputButtons.Trigger),
UxrAvatar.LocalAvatarInput.GetButtonsPressUp(grabber.Side, UxrInputButtons.Trigger));
}
else
{
// Remote avatars will get the trigger pressed amount from the avatar pose blend amount, because poses are synchronized.
SetTriggerPressedAmount(i, grabber.Avatar.GetCurrentHandPoseBlendValue(grabber.Side));
}
bool shoot = false;
switch (trigger.CycleType)
{
case UxrShotCycle.ManualReload:
{
shoot = runtimeTrigger.TriggerPressStarted && runtimeTrigger.HasReloaded;
if (shoot)
{
runtimeTrigger.HasReloaded = false;
}
break;
}
case UxrShotCycle.SemiAutomatic:
shoot = runtimeTrigger.TriggerPressStarted;
break;
case UxrShotCycle.FullyAutomatic:
shoot = runtimeTrigger.TriggerPressed;
break;
}
if (runtimeTrigger.TriggerPressStarted && GetAmmoLeft(i) == 0)
{
trigger.ShotAudioNoAmmo?.Play(trigger.TriggerTransform != null ? trigger.TriggerTransform.position : trigger.TriggerGrabbable.GetGrabPointGrabProximityTransform(grabber, trigger.GrabbableGrabPointIndex).position);
}
if (shoot)
{
// Shoot!
if (TryToShootRound(i))
{
if (grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
// Send haptic to the hand grabbing the grip
UxrAvatar.LocalAvatarInput.SendHapticFeedback(grabber.Side, trigger.ShotHapticClip);
// Send haptic to the other hand if it is also grabbing the weapon
if (UxrGrabManager.Instance.IsHandGrabbing(grabber.Avatar, trigger.TriggerGrabbable, grabber.OppositeSide, true))
{
UxrAvatar.LocalAvatarInput.SendHapticFeedback(grabber.OppositeSide, trigger.ShotHapticClip);
}
}
}
}
if (grabber.Avatar.AvatarMode == UxrAvatarMode.Local && runtimeTrigger.TriggerPressEnded)
{
// Sync ammo after shooting a fully automatic weapon to make sure the ammo left is the same.
if (trigger.CycleType == UxrShotCycle.FullyAutomatic)
{
SyncAmmoLeft(i, GetAmmoLeft(i));
}
}
}
runtimeTrigger.TriggerPressStarted = false;
runtimeTrigger.TriggerPressEnded = false;
if (runtimeTrigger.LastShotTimer > 0.0f)
{
runtimeTrigger.LastShotTimer -= Time.deltaTime;
}
if (runtimeTrigger.RecoilTimer > 0.0f)
{
runtimeTrigger.RecoilTimer -= Time.deltaTime;
}
}
}
///
/// Called when a mag is removed.
///
/// Event sender
/// Event parameters
private void MagTarget_Removed(object sender, UxrManipulationEventArgs e)
{
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (e.GrabbableAnchor == trigger.AmmunitionMagAnchor)
{
Collider magCollider = e.GrabbableObject.GetComponentInChildren();
if (magCollider != null)
{
magCollider.enabled = true;
}
}
}
}
///
/// Called when a mag is attached.
///
/// Event sender
/// Event parameters
private void MagTarget_Placed(object sender, UxrManipulationEventArgs e)
{
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (e.GrabbableAnchor == trigger.AmmunitionMagAnchor)
{
Collider magCollider = e.GrabbableObject.GetComponentInChildren();
if (magCollider != null)
{
magCollider.enabled = false;
}
}
}
}
///
/// Called right after applying constraints to the main grabbable object. It is used to apply the recoil after the
/// constraints to do it in the appropriate order.
///
/// Event sender
/// Event parameters
private void RootGrabbable_ConstraintsApplied(object sender, UxrApplyConstraintsEventArgs e)
{
// Get the grabbable object and apply recoil depending on the number of hands that are grabbing the gun
UxrGrabbableObject grabbableObject = sender as UxrGrabbableObject;
Transform grabbableTransform = grabbableObject.transform;
int grabbingHandCount = UxrGrabManager.Instance.GetGrabbingHandCount(grabbableObject);
for (int i = 0; i < _triggers.Count; i++)
{
UxrFirearmTrigger trigger = _triggers[i];
if (_runtimeTriggers.TryGetValue(i, out RuntimeTriggerInfo runtimeTrigger) && runtimeTrigger.RecoilTimer > 0.0f)
{
float recoilT = trigger.RecoilDurationSeconds > 0.0f ? (trigger.RecoilDurationSeconds - runtimeTrigger.RecoilTimer) / trigger.RecoilDurationSeconds : 0.0f;
Vector3 recoilRight = _recoilAxes != null ? _recoilAxes.right : grabbableTransform.right;
Vector3 recoilUp = _recoilAxes != null ? _recoilAxes.up : grabbableTransform.up;
Vector3 recoilForward = _recoilAxes != null ? _recoilAxes.forward : grabbableTransform.forward;
Vector3 recoilPosition = _recoilAxes != null ? _recoilAxes.position : grabbableTransform.position;
float amplitude = 1.0f - recoilT;
Vector3 recoilOffset = grabbingHandCount == 1 ? amplitude * trigger.RecoilOffsetOneHand : amplitude * trigger.RecoilOffsetTwoHands;
grabbableTransform.position += recoilRight * recoilOffset.x + recoilUp * recoilOffset.y + recoilForward * recoilOffset.z;
grabbableTransform.RotateAround(recoilPosition, -recoilRight, grabbingHandCount == 1 ? amplitude * trigger.RecoilAngleOneHand : amplitude * trigger.RecoilAngleTwoHands);
}
}
}
#endregion
#region Event Trigger Methods
///
/// Event trigger for .
///
/// The weapon trigger index
protected virtual void OnProjectileShot(int triggerIndex)
{
ProjectileShot?.Invoke(triggerIndex);
}
#endregion
#region Private Methods
///
/// Sets the trigger pressed state, to sync multiplayer.
///
/// The trigger index
/// Whether the trigger is pressed
/// Whether the trigger just started being pressed down
/// Whether the trigger just started being released
private void SyncTriggerPressStates(int triggerIndex, bool pressed, bool pressDown, bool pressUp)
{
if (_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
if (runtimeTrigger.TriggerPressed != pressed || runtimeTrigger.TriggerPressStarted != pressDown || runtimeTrigger.TriggerPressEnded != pressUp)
{
BeginSync();
runtimeTrigger.TriggerPressed = pressed;
runtimeTrigger.TriggerPressStarted = pressDown;
runtimeTrigger.TriggerPressEnded = pressUp;
EndSyncMethod(new object[] { triggerIndex, pressed, pressDown, pressUp });
}
}
}
///
/// See .
///
/// The grabbable object for the trigger
private void SyncAmmoLeft(UxrGrabbableObject grabbableTrigger)
{
UxrFirearmTrigger trigger = _triggers.FirstOrDefault(t => t.TriggerGrabbable == grabbableTrigger);
if (trigger != null && trigger.CycleType == UxrShotCycle.FullyAutomatic)
{
int index = _triggers.IndexOf(trigger);
if (index != -1)
{
SyncAmmoLeft(index, GetAmmoLeft(index));
}
}
}
///
/// Sets the ammo left, to sync multiplayer after a fully automatic gun stopped firing.
///
/// The trigger index
/// The ammo left
private void SyncAmmoLeft(int triggerIndex, int ammo)
{
BeginSync();
SetAmmoLeft(triggerIndex, ammo);
EndSyncMethod(new object[] { triggerIndex, ammo });
}
#endregion
#region Private Types & Data
///
/// Gets the root grabbable object.
///
private UxrGrabbableObject RootGrabbable
{
get
{
UxrGrabbableObject firstTriggerGrabbable = _triggers.Count > 0 && _triggers[0].TriggerGrabbable != null ? _triggers[0].TriggerGrabbable : null;
if (firstTriggerGrabbable)
{
// A normal setup will have just one grabbable point but for rifles and weapons with multiple parts we may have different grabbable objects.
// We will just get the root one so that we can subscribe to its ApplyConstraints event to apply recoil effects
Transform weaponRootGrabbableTransform = firstTriggerGrabbable.UsesGrabbableParentDependency ? firstTriggerGrabbable.GrabbableParent.transform : firstTriggerGrabbable.transform;
UxrGrabbableObject rootGrabbable = weaponRootGrabbableTransform.GetComponent();
return rootGrabbable;
}
return null;
}
}
private UxrProjectileSource _weaponSource;
private Dictionary _runtimeTriggers = new Dictionary();
#endregion
}
}