This commit is contained in:
Alex38Lyon
2025-06-03 12:00:47 +02:00
parent ed8041abcd
commit 878ea46cac
1300 changed files with 527178 additions and 0 deletions
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using UnityEngine;
namespace Unity.FPS.Game
{
public class DamageArea : MonoBehaviour
{
[Tooltip("Area of damage when the projectile hits something")]
public float AreaOfEffectDistance = 5f;
[Tooltip("Damage multiplier over distance for area of effect")]
public AnimationCurve DamageRatioOverDistance;
[Header("Debug")] [Tooltip("Color of the area of effect radius")]
public Color AreaOfEffectColor = Color.red * 0.5f;
public void InflictDamageInArea(float damage, Vector3 center, LayerMask layers,
QueryTriggerInteraction interaction, GameObject owner)
{
Dictionary<Health, Damageable> uniqueDamagedHealths = new Dictionary<Health, Damageable>();
// Create a collection of unique health components that would be damaged in the area of effect (in order to avoid damaging a same entity multiple times)
Collider[] affectedColliders = Physics.OverlapSphere(center, AreaOfEffectDistance, layers, interaction);
foreach (var coll in affectedColliders)
{
Damageable damageable = coll.GetComponent<Damageable>();
if (damageable)
{
Health health = damageable.GetComponentInParent<Health>();
if (health && !uniqueDamagedHealths.ContainsKey(health))
{
uniqueDamagedHealths.Add(health, damageable);
}
}
}
// Apply damages with distance falloff
foreach (Damageable uniqueDamageable in uniqueDamagedHealths.Values)
{
float distance = Vector3.Distance(uniqueDamageable.transform.position, transform.position);
uniqueDamageable.InflictDamage(
damage * DamageRatioOverDistance.Evaluate(distance / AreaOfEffectDistance), true, owner);
}
}
void OnDrawGizmosSelected()
{
Gizmos.color = AreaOfEffectColor;
Gizmos.DrawSphere(transform.position, AreaOfEffectDistance);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0244d6b9e98532469c7be0732490a61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class Damageable : MonoBehaviour
{
[Tooltip("Multiplier to apply to the received damage")]
public float DamageMultiplier = 1f;
[Range(0, 1)] [Tooltip("Multiplier to apply to self damage")]
public float SensibilityToSelfdamage = 0.5f;
public Health Health { get; private set; }
void Awake()
{
// find the health component either at the same level, or higher in the hierarchy
Health = GetComponent<Health>();
if (!Health)
{
Health = GetComponentInParent<Health>();
}
}
public void InflictDamage(float damage, bool isExplosionDamage, GameObject damageSource)
{
if (Health)
{
var totalDamage = damage;
// skip the crit multiplier if it's from an explosion
if (!isExplosionDamage)
{
totalDamage *= DamageMultiplier;
}
// potentially reduce damages if inflicted by self
if (Health.gameObject == damageSource)
{
totalDamage *= SensibilityToSelfdamage;
}
// apply the damages
Health.TakeDamage(totalDamage, damageSource);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9b0ce1bb69c96b49a7ac1098a7f5796
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using UnityEngine;
namespace Unity.FPS.Game
{
public class Destructable : MonoBehaviour
{
Health m_Health;
void Start()
{
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, Destructable>(m_Health, this, gameObject);
// Subscribe to damage & death actions
m_Health.OnDie += OnDie;
m_Health.OnDamaged += OnDamaged;
}
void OnDamaged(float damage, GameObject damageSource)
{
// TODO: damage reaction
}
void OnDie()
{
// this will call the OnDestroy function
Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f0914267284bd7468f8ac89253e1557
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+87
View File
@@ -0,0 +1,87 @@
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public class Health : MonoBehaviour
{
[Tooltip("Maximum amount of health")] public float MaxHealth = 10f;
[Tooltip("Health ratio at which the critical health vignette starts appearing")]
public float CriticalHealthRatio = 0.3f;
public UnityAction<float, GameObject> OnDamaged;
public UnityAction<float> OnHealed;
public UnityAction OnDie;
public float CurrentHealth { get; set; }
public bool Invincible { get; set; }
public bool CanPickup() => CurrentHealth < MaxHealth;
public float GetRatio() => CurrentHealth / MaxHealth;
public bool IsCritical() => GetRatio() <= CriticalHealthRatio;
bool m_IsDead;
void Start()
{
CurrentHealth = MaxHealth;
}
public void Heal(float healAmount)
{
float healthBefore = CurrentHealth;
CurrentHealth += healAmount;
CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
// call OnHeal action
float trueHealAmount = CurrentHealth - healthBefore;
if (trueHealAmount > 0f)
{
OnHealed?.Invoke(trueHealAmount);
}
}
public void TakeDamage(float damage, GameObject damageSource)
{
if (Invincible)
return;
float healthBefore = CurrentHealth;
CurrentHealth -= damage;
CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
// call OnDamage action
float trueDamageAmount = healthBefore - CurrentHealth;
if (trueDamageAmount > 0f)
{
OnDamaged?.Invoke(trueDamageAmount, damageSource);
}
HandleDeath();
}
public void Kill()
{
CurrentHealth = 0f;
// call OnDamage action
OnDamaged?.Invoke(MaxHealth, null);
HandleDeath();
}
void HandleDeath()
{
if (m_IsDead)
return;
// call OnDie action
if (CurrentHealth <= 0f)
{
m_IsDead = true;
OnDie?.Invoke();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e27364eefd6dd0341ba72a3bfc0d9727
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,62 @@
using System;
using UnityEngine;
namespace Unity.FPS.Game
{
public abstract class Objective : MonoBehaviour
{
[Tooltip("Name of the objective that will be shown on screen")]
public string Title;
[Tooltip("Short text explaining the objective that will be shown on screen")]
public string Description;
[Tooltip("Whether the objective is required to win or not")]
public bool IsOptional;
[Tooltip("Delay before the objective becomes visible")]
public float DelayVisible;
public bool IsCompleted { get; private set; }
public bool IsBlocking() => !(IsOptional || IsCompleted);
public static event Action<Objective> OnObjectiveCreated;
public static event Action<Objective> OnObjectiveCompleted;
protected virtual void Start()
{
OnObjectiveCreated?.Invoke(this);
DisplayMessageEvent displayMessage = Events.DisplayMessageEvent;
displayMessage.Message = Title;
displayMessage.DelayBeforeDisplay = 0.0f;
EventManager.Broadcast(displayMessage);
}
public void UpdateObjective(string descriptionText, string counterText, string notificationText)
{
ObjectiveUpdateEvent evt = Events.ObjectiveUpdateEvent;
evt.Objective = this;
evt.DescriptionText = descriptionText;
evt.CounterText = counterText;
evt.NotificationText = notificationText;
evt.IsComplete = IsCompleted;
EventManager.Broadcast(evt);
}
public void CompleteObjective(string descriptionText, string counterText, string notificationText)
{
IsCompleted = true;
ObjectiveUpdateEvent evt = Events.ObjectiveUpdateEvent;
evt.Objective = this;
evt.DescriptionText = descriptionText;
evt.CounterText = counterText;
evt.NotificationText = notificationText;
evt.IsComplete = IsCompleted;
EventManager.Broadcast(evt);
OnObjectiveCompleted?.Invoke(this);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7535462baa2ed0d469f708cbe145b720
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public abstract class ProjectileBase : MonoBehaviour
{
public GameObject Owner { get; private set; }
public Vector3 InitialPosition { get; private set; }
public Vector3 InitialDirection { get; private set; }
public Vector3 InheritedMuzzleVelocity { get; private set; }
public float InitialCharge { get; private set; }
public UnityAction OnShoot;
public void Shoot(WeaponController controller)
{
Owner = controller.Owner;
InitialPosition = transform.position;
InitialDirection = transform.forward;
InheritedMuzzleVelocity = controller.MuzzleWorldVelocity;
InitialCharge = controller.CurrentCharge;
OnShoot?.Invoke();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1962d3d25050a4a44819b089007baf39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,502 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Game
{
public enum WeaponShootType
{
Manual,
Automatic,
Charge,
}
[System.Serializable]
public struct CrosshairData
{
[Tooltip("The image that will be used for this weapon's crosshair")]
public Sprite CrosshairSprite;
[Tooltip("The size of the crosshair image")]
public int CrosshairSize;
[Tooltip("The color of the crosshair image")]
public Color CrosshairColor;
}
[RequireComponent(typeof(AudioSource))]
public class WeaponController : MonoBehaviour
{
[Header("Information")] [Tooltip("The name that will be displayed in the UI for this weapon")]
public string WeaponName;
[Tooltip("The image that will be displayed in the UI for this weapon")]
public Sprite WeaponIcon;
[Tooltip("Default data for the crosshair")]
public CrosshairData CrosshairDataDefault;
[Tooltip("Data for the crosshair when targeting an enemy")]
public CrosshairData CrosshairDataTargetInSight;
[Header("Internal References")]
[Tooltip("The root object for the weapon, this is what will be deactivated when the weapon isn't active")]
public GameObject WeaponRoot;
[Tooltip("Tip of the weapon, where the projectiles are shot")]
public Transform WeaponMuzzle;
[Header("Shoot Parameters")] [Tooltip("The type of weapon wil affect how it shoots")]
public WeaponShootType ShootType;
[Tooltip("The projectile prefab")] public ProjectileBase ProjectilePrefab;
[Tooltip("Minimum duration between two shots")]
public float DelayBetweenShots = 0.5f;
[Tooltip("Angle for the cone in which the bullets will be shot randomly (0 means no spread at all)")]
public float BulletSpreadAngle = 0f;
[Tooltip("Amount of bullets per shot")]
public int BulletsPerShot = 1;
[Tooltip("Force that will push back the weapon after each shot")] [Range(0f, 2f)]
public float RecoilForce = 1;
[Tooltip("Ratio of the default FOV that this weapon applies while aiming")] [Range(0f, 1f)]
public float AimZoomRatio = 1f;
[Tooltip("Translation to apply to weapon arm when aiming with this weapon")]
public Vector3 AimOffset;
[Header("Ammo Parameters")]
[Tooltip("Should the player manually reload")]
public bool AutomaticReload = true;
[Tooltip("Has physical clip on the weapon and ammo shells are ejected when firing")]
public bool HasPhysicalBullets = false;
[Tooltip("Number of bullets in a clip")]
public int ClipSize = 30;
[Tooltip("Bullet Shell Casing")]
public GameObject ShellCasing;
[Tooltip("Weapon Ejection Port for physical ammo")]
public Transform EjectionPort;
[Tooltip("Force applied on the shell")]
[Range(0.0f, 5.0f)] public float ShellCasingEjectionForce = 2.0f;
[Tooltip("Maximum number of shell that can be spawned before reuse")]
[Range(1, 30)] public int ShellPoolSize = 1;
[Tooltip("Amount of ammo reloaded per second")]
public float AmmoReloadRate = 1f;
[Tooltip("Delay after the last shot before starting to reload")]
public float AmmoReloadDelay = 2f;
[Tooltip("Maximum amount of ammo in the gun")]
public int MaxAmmo = 8;
[Header("Charging parameters (charging weapons only)")]
[Tooltip("Trigger a shot when maximum charge is reached")]
public bool AutomaticReleaseOnCharged;
[Tooltip("Duration to reach maximum charge")]
public float MaxChargeDuration = 2f;
[Tooltip("Initial ammo used when starting to charge")]
public float AmmoUsedOnStartCharge = 1f;
[Tooltip("Additional ammo used when charge reaches its maximum")]
public float AmmoUsageRateWhileCharging = 1f;
[Header("Audio & Visual")]
[Tooltip("Optional weapon animator for OnShoot animations")]
public Animator WeaponAnimator;
[Tooltip("Prefab of the muzzle flash")]
public GameObject MuzzleFlashPrefab;
[Tooltip("Unparent the muzzle flash instance on spawn")]
public bool UnparentMuzzleFlash;
[Tooltip("sound played when shooting")]
public AudioClip ShootSfx;
[Tooltip("Sound played when changing to this weapon")]
public AudioClip ChangeWeaponSfx;
[Tooltip("Continuous Shooting Sound")] public bool UseContinuousShootSound = false;
public AudioClip ContinuousShootStartSfx;
public AudioClip ContinuousShootLoopSfx;
public AudioClip ContinuousShootEndSfx;
AudioSource m_ContinuousShootAudioSource = null;
bool m_WantsToShoot = false;
public UnityAction OnShoot;
public event Action OnShootProcessed;
int m_CarriedPhysicalBullets;
float m_CurrentAmmo;
float m_LastTimeShot = Mathf.NegativeInfinity;
public float LastChargeTriggerTimestamp { get; private set; }
Vector3 m_LastMuzzlePosition;
public GameObject Owner { get; set; }
public GameObject SourcePrefab { get; set; }
public bool IsCharging { get; private set; }
public float CurrentAmmoRatio { get; private set; }
public bool IsWeaponActive { get; private set; }
public bool IsCooling { get; private set; }
public float CurrentCharge { get; private set; }
public Vector3 MuzzleWorldVelocity { get; private set; }
public float GetAmmoNeededToShoot() =>
(ShootType != WeaponShootType.Charge ? 1f : Mathf.Max(1f, AmmoUsedOnStartCharge)) /
(MaxAmmo * BulletsPerShot);
public int GetCarriedPhysicalBullets() => m_CarriedPhysicalBullets;
public int GetCurrentAmmo() => Mathf.FloorToInt(m_CurrentAmmo);
AudioSource m_ShootAudioSource;
public bool IsReloading { get; private set; }
const string k_AnimAttackParameter = "Attack";
private Queue<Rigidbody> m_PhysicalAmmoPool;
void Awake()
{
m_CurrentAmmo = MaxAmmo;
m_CarriedPhysicalBullets = HasPhysicalBullets ? ClipSize : 0;
m_LastMuzzlePosition = WeaponMuzzle.position;
m_ShootAudioSource = GetComponent<AudioSource>();
DebugUtility.HandleErrorIfNullGetComponent<AudioSource, WeaponController>(m_ShootAudioSource, this,
gameObject);
if (UseContinuousShootSound)
{
m_ContinuousShootAudioSource = gameObject.AddComponent<AudioSource>();
m_ContinuousShootAudioSource.playOnAwake = false;
m_ContinuousShootAudioSource.clip = ContinuousShootLoopSfx;
m_ContinuousShootAudioSource.outputAudioMixerGroup =
AudioUtility.GetAudioGroup(AudioUtility.AudioGroups.WeaponShoot);
m_ContinuousShootAudioSource.loop = true;
}
if (HasPhysicalBullets)
{
m_PhysicalAmmoPool = new Queue<Rigidbody>(ShellPoolSize);
for (int i = 0; i < ShellPoolSize; i++)
{
GameObject shell = Instantiate(ShellCasing, transform);
shell.SetActive(false);
m_PhysicalAmmoPool.Enqueue(shell.GetComponent<Rigidbody>());
}
}
}
public void AddCarriablePhysicalBullets(int count) => m_CarriedPhysicalBullets = Mathf.Max(m_CarriedPhysicalBullets + count, MaxAmmo);
void ShootShell()
{
Rigidbody nextShell = m_PhysicalAmmoPool.Dequeue();
nextShell.transform.position = EjectionPort.transform.position;
nextShell.transform.rotation = EjectionPort.transform.rotation;
nextShell.gameObject.SetActive(true);
nextShell.transform.SetParent(null);
nextShell.collisionDetectionMode = CollisionDetectionMode.Continuous;
nextShell.AddForce(nextShell.transform.up * ShellCasingEjectionForce, ForceMode.Impulse);
m_PhysicalAmmoPool.Enqueue(nextShell);
}
void PlaySFX(AudioClip sfx) => AudioUtility.CreateSFX(sfx, transform.position, AudioUtility.AudioGroups.WeaponShoot, 0.0f);
void Reload()
{
if (m_CarriedPhysicalBullets > 0)
{
m_CurrentAmmo = Mathf.Min(m_CarriedPhysicalBullets, ClipSize);
}
IsReloading = false;
}
public void StartReloadAnimation()
{
if (m_CurrentAmmo < m_CarriedPhysicalBullets)
{
GetComponent<Animator>().SetTrigger("Reload");
IsReloading = true;
}
}
void Update()
{
UpdateAmmo();
UpdateCharge();
UpdateContinuousShootSound();
if (Time.deltaTime > 0)
{
MuzzleWorldVelocity = (WeaponMuzzle.position - m_LastMuzzlePosition) / Time.deltaTime;
m_LastMuzzlePosition = WeaponMuzzle.position;
}
}
void UpdateAmmo()
{
if (AutomaticReload && m_LastTimeShot + AmmoReloadDelay < Time.time && m_CurrentAmmo < MaxAmmo && !IsCharging)
{
// reloads weapon over time
m_CurrentAmmo += AmmoReloadRate * Time.deltaTime;
// limits ammo to max value
m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo, 0, MaxAmmo);
IsCooling = true;
}
else
{
IsCooling = false;
}
if (MaxAmmo == Mathf.Infinity)
{
CurrentAmmoRatio = 1f;
}
else
{
CurrentAmmoRatio = m_CurrentAmmo / MaxAmmo;
}
}
void UpdateCharge()
{
if (IsCharging)
{
if (CurrentCharge < 1f)
{
float chargeLeft = 1f - CurrentCharge;
// Calculate how much charge ratio to add this frame
float chargeAdded = 0f;
if (MaxChargeDuration <= 0f)
{
chargeAdded = chargeLeft;
}
else
{
chargeAdded = (1f / MaxChargeDuration) * Time.deltaTime;
}
chargeAdded = Mathf.Clamp(chargeAdded, 0f, chargeLeft);
// See if we can actually add this charge
float ammoThisChargeWouldRequire = chargeAdded * AmmoUsageRateWhileCharging;
if (ammoThisChargeWouldRequire <= m_CurrentAmmo)
{
// Use ammo based on charge added
UseAmmo(ammoThisChargeWouldRequire);
// set current charge ratio
CurrentCharge = Mathf.Clamp01(CurrentCharge + chargeAdded);
}
}
}
}
void UpdateContinuousShootSound()
{
if (UseContinuousShootSound)
{
if (m_WantsToShoot && m_CurrentAmmo >= 1f)
{
if (!m_ContinuousShootAudioSource.isPlaying)
{
m_ShootAudioSource.PlayOneShot(ShootSfx);
m_ShootAudioSource.PlayOneShot(ContinuousShootStartSfx);
m_ContinuousShootAudioSource.Play();
}
}
else if (m_ContinuousShootAudioSource.isPlaying)
{
m_ShootAudioSource.PlayOneShot(ContinuousShootEndSfx);
m_ContinuousShootAudioSource.Stop();
}
}
}
public void ShowWeapon(bool show)
{
WeaponRoot.SetActive(show);
if (show && ChangeWeaponSfx)
{
// m_ShootAudioSource.PlayOneShot(ChangeWeaponSfx);
}
IsWeaponActive = show;
}
public void UseAmmo(float amount)
{
m_CurrentAmmo = Mathf.Clamp(m_CurrentAmmo - amount, 0f, MaxAmmo);
m_CarriedPhysicalBullets -= Mathf.RoundToInt(amount);
m_CarriedPhysicalBullets = Mathf.Clamp(m_CarriedPhysicalBullets, 0, MaxAmmo);
m_LastTimeShot = Time.time;
}
public bool HandleShootInputs(bool inputDown, bool inputHeld, bool inputUp)
{
m_WantsToShoot = inputDown || inputHeld;
switch (ShootType)
{
case WeaponShootType.Manual:
if (inputDown)
{
return TryShoot();
}
return false;
case WeaponShootType.Automatic:
if (inputHeld)
{
return TryShoot();
}
return false;
case WeaponShootType.Charge:
if (inputHeld)
{
TryBeginCharge();
}
// Check if we released charge or if the weapon shoot autmatically when it's fully charged
if (inputUp || (AutomaticReleaseOnCharged && CurrentCharge >= 1f))
{
return TryReleaseCharge();
}
return false;
default:
return false;
}
}
bool TryShoot()
{
if (m_CurrentAmmo >= 1f
&& m_LastTimeShot + DelayBetweenShots < Time.time)
{
HandleShoot();
m_CurrentAmmo -= 1f;
return true;
}
return false;
}
bool TryBeginCharge()
{
if (!IsCharging
&& m_CurrentAmmo >= AmmoUsedOnStartCharge
&& Mathf.FloorToInt((m_CurrentAmmo - AmmoUsedOnStartCharge) * BulletsPerShot) > 0
&& m_LastTimeShot + DelayBetweenShots < Time.time)
{
UseAmmo(AmmoUsedOnStartCharge);
LastChargeTriggerTimestamp = Time.time;
IsCharging = true;
return true;
}
return false;
}
bool TryReleaseCharge()
{
if (IsCharging)
{
HandleShoot();
CurrentCharge = 0f;
IsCharging = false;
return true;
}
return false;
}
void HandleShoot()
{
int bulletsPerShotFinal = ShootType == WeaponShootType.Charge
? Mathf.CeilToInt(CurrentCharge * BulletsPerShot)
: BulletsPerShot;
// spawn all bullets with random direction
for (int i = 0; i < bulletsPerShotFinal; i++)
{
Vector3 shotDirection = GetShotDirectionWithinSpread(WeaponMuzzle);
ProjectileBase newProjectile = Instantiate(ProjectilePrefab, WeaponMuzzle.position,
Quaternion.LookRotation(shotDirection));
newProjectile.Shoot(this);
}
// muzzle flash
if (MuzzleFlashPrefab != null)
{
GameObject muzzleFlashInstance = Instantiate(MuzzleFlashPrefab, WeaponMuzzle.position,
WeaponMuzzle.rotation, WeaponMuzzle.transform);
// Unparent the muzzleFlashInstance
if (UnparentMuzzleFlash)
{
muzzleFlashInstance.transform.SetParent(null);
}
Destroy(muzzleFlashInstance, 2f);
}
if (HasPhysicalBullets)
{
ShootShell();
m_CarriedPhysicalBullets--;
}
m_LastTimeShot = Time.time;
// play shoot SFX
if (ShootSfx && !UseContinuousShootSound)
{
m_ShootAudioSource.PlayOneShot(ShootSfx);
}
// Trigger attack animation if there is any
if (WeaponAnimator)
{
WeaponAnimator.SetTrigger(k_AnimAttackParameter);
}
OnShoot?.Invoke();
OnShootProcessed?.Invoke();
}
public Vector3 GetShotDirectionWithinSpread(Transform shootTransform)
{
float spreadAngleRatio = BulletSpreadAngle / 180f;
Vector3 spreadWorldDirection = Vector3.Slerp(shootTransform.forward, UnityEngine.Random.insideUnitSphere,
spreadAngleRatio);
return spreadWorldDirection;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ad90ba0b1477764da373f5ec01a8cb5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: