// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using UltimateXR.Core; using UltimateXR.Core.Components.Singleton; using UltimateXR.Core.Settings; using UnityEngine; namespace UltimateXR.Mechanics.Weapons { /// /// Singleton manager in charge of updating projectiles, computing hits against entities and damage done on /// components. /// public partial class UxrWeaponManager : UxrSingleton { #region Public Types & Data /// /// Event triggered right before an is about to receive damage. /// Setting will allow not to take the damage. /// public event EventHandler DamageReceiving; /// /// Event triggered right after the actor received damage. /// Setting is not supported, since the damage was already taken. /// public event EventHandler DamageReceived; /// /// Event called whenever there was a projectile impact but no was involved. Mostly hits /// against the scenario that still generate decals, FX, etc. /// public event EventHandler NonActorImpacted; #endregion #region Public Methods /// /// Updates the manager. /// public void UpdateManager() { UpdateProjectiles(); } /// /// Registers a new projectile shot so that it gets automatically update by the manager from that moment until it hits /// something or gets destroyed. /// /// Projectile source /// Shot descriptor /// World position /// World orientation. The projectile will travel in the forward (z) direction public void RegisterNewProjectileShot(UxrProjectileSource projectileSource, UxrShotDescriptor shotDescriptor, Vector3 position, Quaternion orientation) { _projectiles.Add(new ProjectileInfo(projectileSource.TryGetWeaponOwner(), projectileSource, shotDescriptor, position, orientation)); } /// /// Applies radius damage to all elements around a source position. /// /// The actor that was responsible for the damage or null if there wasn't any /// Explosion world position /// Radius /// Damage at the very same point of the explosion /// Damage at the distance set by public void ApplyRadiusDamage(UxrActor actorSource, Vector3 position, float radius, float nearDamage, float farDamage) { foreach (KeyValuePair damageActorPair in _damageActors) { float distance = Vector3.Distance(damageActorPair.Value.Transform.position, position); if (distance < radius) { float damage = Mathf.Lerp(nearDamage, farDamage, distance / radius); damageActorPair.Key.ReceiveExplosion(actorSource, position, damage); } } } #endregion #region Unity /// /// Initializes the manager. Subscribes to actor enable/disable events. /// protected override void Awake() { base.Awake(); UxrActor.GlobalEnabled += Actor_Enabled; UxrActor.GlobalDisabled += Actor_Disabled; } /// /// Initializes the manager. Unsubscribes from actor enable/disable events. /// protected override void OnDestroy() { base.OnDestroy(); UxrActor.GlobalEnabled -= Actor_Enabled; UxrActor.GlobalDisabled -= Actor_Disabled; } #endregion #region Event Handling Methods /// /// Called whenever an was enabled. Create a new entry in the internal dictionary. /// /// Actor that was enabled private void Actor_Enabled(UxrActor actor) { if (_damageActors.ContainsKey(actor) == false) { _damageActors.Add(actor, new ActorInfo(actor)); actor.DamageReceiving += Actor_DamageReceiving; actor.DamageReceived += Actor_DamageReceived; } } /// /// Called whenever an was disabled. Remove the entry from the internal dictionary. /// /// Actor that was disabled private void Actor_Disabled(UxrActor actor) { if (_damageActors.ContainsKey(actor)) { _damageActors.Remove(actor); actor.DamageReceiving -= Actor_DamageReceiving; actor.DamageReceived -= Actor_DamageReceived; } } /// /// Called whenever an actor is about to receive any damage. The damage can be canceled using /// . /// /// Event sender /// Event parameters private void Actor_DamageReceiving(object sender, UxrDamageEventArgs e) { OnDamageReceiving(e); } /// /// Called right after an actor received any damage. Damage can not be canceled using /// because it was already taken. /// /// Event sender /// Event parameters private void Actor_DamageReceived(object sender, UxrDamageEventArgs e) { OnDamageReceived(e); } #endregion #region Event Trigger Methods /// /// Event trigger for . /// /// Event parameters private void OnDamageReceiving(UxrDamageEventArgs e) { DamageReceiving?.Invoke(this, e); } /// /// Event trigger for . /// /// Event parameters private void OnDamageReceived(UxrDamageEventArgs e) { if (UxrGlobalSettings.Instance.LogLevelWeapons >= UxrLogLevel.Relevant) { string sourceInfo = e.ActorSource != null ? $" from actor {e.ActorSource.name}." : string.Empty; if (UxrGlobalSettings.Instance.LogLevelWeapons >= UxrLogLevel.Relevant) { Debug.Log($"{UxrConstants.WeaponsModule}: Actor {e.ActorTarget.name} received {e.Damage} damage of type {e.DamageType}{sourceInfo}. Is dead? {e.Dies}."); } } DamageReceived?.Invoke(this, e); } #endregion #region Private Methods /// /// Updates all current projectiles. /// private void UpdateProjectiles() { for (int i = 0; i < _projectiles.Count; ++i) { if (_projectiles[i].Projectile == null) { // Dynamic instance removed somewhere _projectiles.RemoveAt(i); i--; continue; } if (_projectiles[i].FirstFrame) { // Render first frame where the weapon is _projectiles[i].FirstFrame = false; continue; } Vector3 oldPos = _projectiles[i].ProjectileLastPosition; Vector3 newPos = _projectiles[i].Projectile.transform.position; Vector3 projectileForward = _projectiles[i].Projectile.transform.forward; if (_projectiles[i].ShotDescriptor.UseAutomaticProjectileTrajectory) { newPos = oldPos + (_projectiles[i].ProjectileSpeed * Time.deltaTime * projectileForward); _projectiles[i].Projectile.transform.position = newPos; } _projectiles[i].ProjectileLastPosition = newPos; _projectiles[i].ProjectileDistanceTravelled += Vector3.Distance(oldPos, newPos); if (_projectiles[i].ProjectileDistanceTravelled >= _projectiles[i].ShotDescriptor.ProjectileMaxDistance) { // Max distance travelled Destroy(_projectiles[i].Projectile); _projectiles.RemoveAt(i); i--; continue; } if (_projectiles[i].ShotDescriptor.UseAutomaticProjectileTrajectory) { float rayLength = Vector3.Distance(oldPos, newPos + projectileForward * _projectiles[i].ShotDescriptor.ProjectileLength); if (Physics.Raycast(oldPos, projectileForward, out RaycastHit raycastHit, rayLength, _projectiles[i].ShotLayerMask, QueryTriggerInteraction.Ignore)) { UxrProjectileDeflect projectileDeflect = raycastHit.collider.GetComponentInParent(); // Has UxrProjectileDeflect component? if (projectileDeflect != null) { Vector3 newForward = Vector3.Reflect(projectileForward, raycastHit.normal).normalized; _projectiles[i].Projectile.transform.SetPositionAndRotation(raycastHit.point + newForward * 0.05f, Quaternion.LookRotation(newForward)); // + rayLength - raycastHit.distance)); _projectiles[i].ProjectileDeflectSource = projectileDeflect; projectileDeflect.AudioDeflect?.Play(raycastHit.point); if (projectileDeflect.DecalOnReflect != null) { // Generate decal? UxrImpactDecal.CheckCreateDecal(raycastHit, -1, projectileDeflect.DecalOnReflect, projectileDeflect.DecalLife, projectileDeflect.DecalFadeoutDuration, projectileDeflect.TwoSidedDecal, projectileDeflect.TwoSidedDecalThickness); } _projectiles[i].AddShotLayerMask(projectileDeflect.CollideLayersAddOnReflect.value); _projectiles[i].ProjectileLastPosition = _projectiles[i].Projectile.transform.position; projectileDeflect.RaiseProjectileDeflected(new UxrDeflectEventArgs(_projectiles[i].ProjectileSource, raycastHit, newForward)); } else { UxrActor targetActor = raycastHit.collider.GetComponentInParent(); if (targetActor != null) { // Impact with an actor. float normalizedDistance = _projectiles[i].ProjectileDistanceTravelled / _projectiles[i].ShotDescriptor.ProjectileMaxDistance; float damage = Mathf.Lerp(_projectiles[i].ShotDescriptor.ProjectileDamageNear, _projectiles[i].ShotDescriptor.ProjectileDamageFar, normalizedDistance); if (_projectiles[i].ProjectileDeflectSource != null) { // Came from a shot deflected by a UxrProjectileDeflect targetActor.ReceiveImpact(_projectiles[i].ProjectileDeflectSource.Owner, raycastHit, damage); } else { // Direct hit from a projectile targetActor.ReceiveImpact(_projectiles[i].ProjectileSource.TryGetWeaponOwner(), raycastHit, damage); } } else { // Impact with something that isn't an actor, probably the scenario. if (_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpact) { Vector3 instantiateForward = Vector3.Reflect(projectileForward, raycastHit.normal).normalized; GameObject newInstance = Instantiate(_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpact, raycastHit.point, Quaternion.LookRotation(instantiateForward)); if (_projectiles[i].ShotDescriptor.PrefabInstantiateOnImpactLife >= 0.0f) { Destroy(newInstance, _projectiles[i].ShotDescriptor.PrefabInstantiateOnImpactLife); } } Rigidbody rigidbody = raycastHit.collider.GetComponentInParent(); if (rigidbody != null) { rigidbody.AddForceAtPosition(_projectiles[i].ProjectileSpeed * _projectiles[i].ShotDescriptor.ProjectileImpactForceMultiplier * projectileForward, raycastHit.transform.position); } UxrOverrideImpactDecal overrideDecal = raycastHit.collider.GetComponentInParent(); // Generate decal? UxrImpactDecal.CheckCreateDecal(raycastHit, _projectiles[i].ShotDescriptor.CreateDecalLayerMask, overrideDecal != null ? overrideDecal.DecalToUse : _projectiles[i].ShotDescriptor.PrefabScenarioImpactDecal, _projectiles[i].ShotDescriptor.PrefabScenarioImpactDecalLife, _projectiles[i].ShotDescriptor.DecalFadeoutDuration); NonActorImpacted?.Invoke(_projectiles[i].ProjectileSource, new UxrNonDamagingImpactEventArgs(_projectiles[i].ProjectileSource.TryGetWeaponOwner(), _projectiles[i].ProjectileSource, raycastHit)); } Destroy(_projectiles[i].Projectile); _projectiles.RemoveAt(i); i--; } } } } } #endregion #region Private Types & Data private readonly Dictionary _damageActors = new Dictionary(); private readonly List _projectiles = new List(); #endregion } }