Files
dungeons/Assets/UltimateXR/Runtime/Scripts/Mechanics/Weapons/UxrFirearmWeapon.cs
2024-08-06 21:58:35 +02:00

700 lines
27 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrFirearmWeapon.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
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
{
/// <summary>
/// Type of weapon that shoots projectiles. A firearm has one or more <see cref="UxrFirearmTrigger" /> 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 <see cref="UxrFirearmWeapon" /> requires a <see cref="UxrProjectileSource" /> 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 <see cref="UxrProjectileSource" /> will require the same amount of entries in
/// <see cref="UxrProjectileSource.ShotTypes" />.
/// </summary>
[RequireComponent(typeof(UxrProjectileSource))]
public partial class UxrFirearmWeapon : UxrWeapon
{
#region Inspector Properties/Serialized Fields
[SerializeField] protected Transform _recoilAxes;
[SerializeField] private List<UxrFirearmTrigger> _triggers;
#endregion
#region Public Types & Data
/// <summary>
/// Event called right after the weapon shot a projectile using the given trigger index.
/// </summary>
public event Action<int> ProjectileShot;
#endregion
#region Public Methods
/// <summary>
/// Checks whether a trigger is in a loaded state, meaning it is ready to shoot if pressed and there is any ammo left.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <returns>Whether it is ready to shoot</returns>
public bool IsLoaded(int triggerIndex)
{
if (_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return runtimeTrigger.HasReloaded;
}
return false;
}
/// <summary>
/// Sets the given weapon trigger loaded state so that it is ready to shoot if there is ammo left.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
public void Reload(int triggerIndex)
{
if (!_runtimeTriggers.TryGetValue(triggerIndex, out RuntimeTriggerInfo runtimeTrigger))
{
return;
}
BeginSync();
runtimeTrigger.HasReloaded = true;
EndSyncMethod(new object[] { triggerIndex });
}
/// <summary>
/// Checks whether there is a magazine attached that fires shots using the given trigger. It may or may not have ammo.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <returns>Whether there is a magazine attached</returns>
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;
}
/// <summary>
/// Gets the attached magazine maximum capacity.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <returns>Maximum capacity of ammo in the attached magazine. If there isn't any magazine attached it returns 0</returns>
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<UxrFirearmMag>();
return mag != null ? mag.Capacity : 0;
}
return 0;
}
/// <summary>
/// Gets the ammo left in the attached magazine.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <returns>Ammo left in the attached magazine. If there isn't any magazine attached it returns 0</returns>
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<UxrFirearmMag>();
return mag != null ? mag.Rounds : 0;
}
}
else
{
return int.MaxValue;
}
return 0;
}
/// <summary>
/// Sets the ammo left in the attached magazine.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <param name="ammo">New ammo</param>
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<UxrFirearmMag>();
if (mag != null)
{
mag.Rounds = ammo;
}
}
}
/// <summary>
/// Sets the trigger pressed amount.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <param name="amount">Pressed amount between range [0.0, 1.0]</param>
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);
}
}
/// <summary>
/// Tries to shoot a round using the given trigger.
/// </summary>
/// <param name="triggerIndex">Index in <see cref="_triggers" /></param>
/// <returns>
/// Whether a round was shot. If no round was shot it can mean that:
/// <list type="bullet">
/// <item>The trigger index references an entry that doesn't exist.</item>
/// <item>The firearm isn't loaded.</item>
/// <item>The firearm doesn't have any ammo left or there is no magazine attached.</item>
/// <item>The shoot frequency doesn't allow to shoot again so quickly.</item>
/// </list>
/// </returns>
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
/// <inheritdoc />
protected override void Awake()
{
base.Awake();
for (int i = 0; i < _triggers.Count; ++i)
{
if (!_runtimeTriggers.ContainsKey(i))
{
_runtimeTriggers.Add(i, new RuntimeTriggerInfo());
}
}
}
/// <summary>
/// Subscribes to events.
/// </summary>
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;
}
}
}
/// <summary>
/// Unsubscribes from events.
/// </summary>
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;
}
}
}
/// <summary>
/// Initializes the component.
/// </summary>
protected override void Start()
{
base.Start();
_weaponSource = GetCachedComponent<UxrProjectileSource>();
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<Collider>();
if (magCollider != null)
{
magCollider.enabled = false;
}
}
}
}
}
#endregion
#region Event Handling Methods
/// <summary>
/// Called when the grip of a grabbable object for a given trigger was released.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event parameters</param>
private void Trigger_Released(object sender, UxrManipulationEventArgs e)
{
if (e.Grabber != null && e.Grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
SyncAmmoLeft(e.GrabbableObject);
}
}
/// <summary>
/// Called when the grip of a grabbable object for a given trigger was placed.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event parameters</param>
private void Trigger_Placed(object sender, UxrManipulationEventArgs e)
{
if (e.Grabber != null && e.Grabber.Avatar.AvatarMode == UxrAvatarMode.Local)
{
SyncAmmoLeft(e.GrabbableObject);
}
}
/// <summary>
/// Called after the avatars have been updated. Updates the hand trigger blend value.
/// </summary>
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;
}
}
}
/// <summary>
/// Called when a mag is removed.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void MagTarget_Removed(object sender, UxrManipulationEventArgs e)
{
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (e.GrabbableAnchor == trigger.AmmunitionMagAnchor)
{
Collider magCollider = e.GrabbableObject.GetComponentInChildren<Collider>();
if (magCollider != null)
{
magCollider.enabled = true;
}
}
}
}
/// <summary>
/// Called when a mag is attached.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
private void MagTarget_Placed(object sender, UxrManipulationEventArgs e)
{
foreach (UxrFirearmTrigger trigger in _triggers)
{
if (e.GrabbableAnchor == trigger.AmmunitionMagAnchor)
{
Collider magCollider = e.GrabbableObject.GetComponentInChildren<Collider>();
if (magCollider != null)
{
magCollider.enabled = false;
}
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sender">Event sender</param>
/// <param name="e">Event parameters</param>
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
/// <summary>
/// Event trigger for <see cref="ProjectileShot" />.
/// </summary>
/// <param name="triggerIndex">The weapon trigger index</param>
protected virtual void OnProjectileShot(int triggerIndex)
{
ProjectileShot?.Invoke(triggerIndex);
}
#endregion
#region Private Methods
/// <summary>
/// Sets the trigger pressed state, to sync multiplayer.
/// </summary>
/// <param name="triggerIndex">The trigger index</param>
/// <param name="pressed">Whether the trigger is pressed</param>
/// <param name="pressDown">Whether the trigger just started being pressed down</param>
/// <param name="pressUp">Whether the trigger just started being released</param>
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 });
}
}
}
/// <summary>
/// See <see cref="SyncAmmoLeft(int, int)" />.
/// </summary>
/// <param name="grabbableTrigger">The grabbable object for the trigger</param>
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));
}
}
}
/// <summary>
/// Sets the ammo left, to sync multiplayer after a fully automatic gun stopped firing.
/// </summary>
/// <param name="triggerIndex"> The trigger index</param>
/// <param name="ammo">The ammo left</param>
private void SyncAmmoLeft(int triggerIndex, int ammo)
{
BeginSync();
SetAmmoLeft(triggerIndex, ammo);
EndSyncMethod(new object[] { triggerIndex, ammo });
}
#endregion
#region Private Types & Data
/// <summary>
/// Gets the root grabbable object.
/// </summary>
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<UxrGrabbableObject>();
return rootGrabbable;
}
return null;
}
}
private UxrProjectileSource _weaponSource;
private Dictionary<int, RuntimeTriggerInfo> _runtimeTriggers = new Dictionary<int, RuntimeTriggerInfo>();
#endregion
}
}